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<int, std::error_code> 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 <tuple> 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; };