Quick: When you design C++ APIs, when and how should you use pair and tuple?
The answer is as simple as it is surprising: Never. Ever.
When we design APIs, we naturally strive for qualities such as readability, ease-of-use, and discoverability. Some C++ types are enablers in this regard: std::optional
, std::variant
, std::string_view
/gsl::string_span
, and, of course, std::unique_ptr
. They help you design good interfaces by providing what we call vocabulary types. These types may be trivial to implement, but, just like STL algorithms, they provide a higher level of abstraction (a vocabulary) in which to reason about code. Unlike algorithms, though, which appear mostly in the implementation, and don't affect APIs much, vocabulary types are at their best when used in APIs.
So, I asked myself, are std::pair
and std::tuple
vocabulary types?
Vocabulary Types
To answer the question, I looked at what these types help model. This should be trivial to answer for vocabulary types, much as it's trivial to remember what an STL algorithm does just by looking at the name.
Take std::optional
, for example. It models a value that may be absent, i.e. an optional value. This is great. When you start looking for uses, you can't help but see lots of opportunities just jumping out at you: std::optional
is the perfect value type for hash tables that use open addressing. The atoi()
return value could finally distinguish between a zero and an error:
But what if you want to return an error code, or a human-readable error description? Then std::optional
is not the prime choice. There should never be an error and a valid parsed value returned from the same invocation of atoi()
, so something like std::variant<int, std::error_code=""></int,>
seems perfect.
But is it?
Consider:
Some people may call this good API, I call it horrible. If you force your users to query the result with a chain of get_if
s, then you are abusing std::variant
, which is designed to contain alternative types with similar purpose, so that it can be efficiently handled using a static visitor. You could use a visitor to handle the result of atoi()
, but is that really an API you'd want to work with?
A New Vocabulary Type
Put yourself into the position of the users of your API. What they want is a std::optional
where the option is not between presence and absence of a value, but between a value and an error code. So, give it to them:
There, we just created a possible new vocabulary type.
But ok, I digress. Back to std::pair
and std::tuple
. What do they model?
As best as I can put it, a std::pair
models a pair of two values, and std::tuple
models a ... tuple of zero to (insert your implementation limit here) values. Sounds simple, but what does that actually mean? Since std::pair
is a subset of std::tuple
, let's restrict ourselves to just tuples.
We have a language construct, inherited from C (boo!), that allows us to package a pair or a triple or ... of values into one object: it's called struct
. It doesn't even have a limit on the arity of the object. Surprise!
Pair Models ... Struct, Tuple Models ... Struct
So, what's the advantage of a tuple over a struct?
I have no idea! But I guess the answer is "none".
Ok, so what's the advantage of a struct over a tuple?
Where should I begin?
First, you get to choose names for the values. The std::set::insert()
function could be as easy to use as
Sadly, what we got is std::pair
:
Second, you can enforce invariants amongst the data members by making them private and mediating access via member functions, as we did in the value_or_error
example, where accessing the value when an error was set would throw an exception.
Third, you can add (convenience) methods to the struct, possibly making it a reusable component in its own right:
None of this is possible if you use std::pair
or std::tuple
in your APIs.
Variadic Woes
There's just one problem with structs: they cannot be variadic (yet), and that's when using tuples as API is somewhat acceptable, because we have nothing else at the moment. But there's hardly a handful of such cases in the standard library, and most deal with implementing std::tuple
in the first place (std::tie()
, std::make_tuple()
, std::forward_as_tuple()
, ...). About the only example that's not in <tuple></tuple>
is the zip()
function that's being discussed:
Conclusion
I hope I could convince you that using std::pair
and std::tuple
in APIs is a bad idea. Or, to say it in the style of Sean Parent: "No raw tuples."
Defining a small class or struct is almost always the superior alternative. C++ currently lacks just one feature that would all but obsolete tuples: a way to define variadic structs. Maybe the static reflection work will yield that mechanism, maybe we need a different mechanism.
In any case, if you are not in that 0.01% of cases where you need variadic return values, then there's already no reason to continue using tuples and pairs in APIs.
Since we're a Qt shop, too, I'll leave you with an example of how even Qt, which somewhat rightfully prides itself for its API design, can get this wrong:
Trusted software excellence across embedded and desktop platforms
The KDAB Group is a globally recognized provider for software consulting, development and training, specializing in embedded devices and complex cross-platform desktop applications. In addition to being leading experts in Qt, C++ and 3D technologies for over two decades, KDAB provides deep expertise across the stack, including Linux, Rust and modern UI frameworks. With 100+ employees from 20 countries and offices in Sweden, Germany, USA, France and UK, we serve clients around the world.
6 Comments
27 - Oct - 2016
Kevin Kofler
Hopefully they won't misspell "color" this way. ;-) (Well, seriously, since the type is called QColor, the member needs to be consistently spelled "color" too.)
28 - Oct - 2016
Philippe
Your value_or_error proposal look like std::expected, right? http://www.hyc.io/boost/expected-proposal.pdf "Class template expected proposed here is a type that may contain a value of type T or a value of type E in its storage space. T represents the expected value, E represents the reason explaining why it doesn’t contains a value of type T, that is the unexpected value."
5 - Feb - 2017
Vicente Botet
I hope we will have soon an unnamed struct like so that we don't need to define a new type just to name the fields.
template insert(...);
See http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0341r0.html
11 - Feb - 2017
Aurelien
I read a similar question about std::pair 10 years ago, and it seems we still haven't figured out when to use one or another.
I would say tuple offers meta-programming : you can iterate over the types of any given tuple for serialization for example. But in general I think it's a well known thing that tuples make code harder to read (and possibly easier to write), and this is not specific to C++.
27 - Aug - 2017
einpoklum
The "problem" with struct is that it's not instrumented like a tuple is - you can't easily get the k'th element of a struct, or its type, specifically. Antony Polukhin has done some work on this, though with his "magic_get".
5 - Feb - 2018
alfC
Nice article.
The advantage of
tuple
overstruct
is at a meta level.tuple
provides (some degree of) introspection (including variadiability as you said). Whether introspection is a good reason to use it, I don't know.Also, it would be cool to be able to define type on the return type.