Tuple And Pair in C++ APIs? A Simple Design Goal to Improve Your C++ APIs
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:
if (auto result = atoi(str)) std::cout << "got " << *result << std::endl; else std::cout >> "failed to parse \" << str << "\" as an integer: " << "???" << std::endl;
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
seems perfect.
But is it?
Consider:
auto result = atoi(str); if (auto i = std::get_if<int>(&result)) std::cout << "got " << i << std::endl; else if (auto error = std::get_if<std::error_code>(&result))) std::cout << "failed to parse \" << str << "\" as an integer: " << error.message() << std::endl; else std::cout << "oops, variant was in invalid state" << std::endl;
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:
template <typename T> class value_or_error { std::variant<T, std::error_code> m_v; // space-saving impl detail public: explicit value_or_error(std::error_code e) : m_v(std::move(e)) {} explicit value_or_error(T t) : m_v(std::move(t)) {} std::error_code error() const { if (auto e = std::get_if<std::error_code>(&m_v)) return *e; else return std::error_code(); } T& operator *() { return std::get<0>(m_v); } T const& operator *() const { return std::get<0>(m_v); } explicit operator bool() const { return !error(); } bool operator !() const { return error(); } };
Usage:
if (auto result = atoi(str)) std::cout << "got " << *result << std::endl; else std::cout << "failed to parse \" << str << "\" as an integer: " << result.error().message() << std::endl;
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
template <typename Iterator> struct insert_result { Iterator iterator; bool inserted; }; auto result = set.insert(obj); if (!result.inserted) *result.iterator = obj;
Sadly, what we got is std::pair
:
auto result = set.insert(obj); if (!result.second) // or was it .first?! - I can never remember... *result.first = obj;
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:
template <typename Iterator>; struct equal_range_result { Iterator first; Iterator last; friend auto begin(const equal_range_result &r) { return r.first; } friend auto end(const equal_range_result &r) { return r.last; } }; for (auto && [key, value] : map.equal_range(obj)) // ...
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
is the zip()
function that’s being discussed:
auto v1 = ...; //... auto vN = ...; for (auto &&e : zip(v1, ..., vN)) // ...
And you need Structured Bindings to make that acceptable:
for (auto && [e1, ..., eN] : zip(v1, ..., vN)) // ...
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:
// Qt 5: typedef QPair<qreal, QColor> QGradientStop;
// Qt 6 (hopefully): struct QGradientStop { qreal location; QColor colour; };
About KDAB
KDAB is a consulting company offering a wide variety of expert services in Qt, C++ and 3D/OpenGL and providing training courses in:
KDAB believes that it is critical for our business to contribute to the Qt framework and C++ thinking, to keep pushing these technologies forward to ensure they remain competitive.
> QColor colour;
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.)
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.”
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
> Ok, so what’s the advantage of a struct over a tuple?
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++.
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”.
Nice article.
The advantage of `tuple` over `struct` 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.
“`
struct insert_result {
std::set::iterator iterator;
bool inserted;
} insert(std::set& set, double const& v){
auto p = set.insert(v);
return {p.first, p.second};
}
“`