In API design, when should I prefer a length-2 std::tuple over a pair?

734 views Asked by At

I'm writing a small library which has some API function returning two things (of different types). I'd rather not declare a struct just for that; so I'm thinking of returning an std::pair<foo, bat>. But - perhaps in these modern times I should prefer returning std::tuple<foo, bar> instead?

More generally, when should a tuple be prefered over a pair, and when is pair the more appropriate construct?

5

There are 5 answers

0
Yakk - Adam Nevraumont On BEST ANSWER

You should declare a struct for that.

get<0>(x) and get<1>(x), or even post-C++11's get<Foo>(x) and get<Bar>(x), are less meaningful and/or idiomatic than x.foo or x.bar.

tuples are best for non-uniformly typed things that are identified by order.

pairs are a tuple that was written prior to C++11.

Both pair and tuple (and array while we are at it) are tuple-like, in that they support std::tuple_size and get<N>.

It has been considered an error in the std library that so many types use pair instead of structures with properly named fields. Ie, if map used struct KV{ Key key; Value value; }, it would have been better.

Now, metaprogramming support for KV as a generic pair would be good as well. So, tuple_size and get<0> etc. But throwing away named fields is generally a bad idea. Names have power.

With C++17, simple structs start working with structured binding, even if you don't make them "tuple-like".

If you do have things whose identity is determined by their order of non-uniform type, tuple is the way to go. pair is a nearly legacy type. There are some advantages to pair over tuple, but tuple continues to be improved to remove them (like implicit initialization of tuple).

0
πάντα ῥεῖ On

But - perhaps in these modern times I should prefer returning std::tuple<foo, bar> instead?

More generally, when should a tuple be prefered over a pair, and when is pair the more appropriate construct?

Well, it might be useful in case you want to work with (meta-)algorithms that rather expect to take a std::tuple than a std::pair.

Finally that is absolutely dependent on your actual use case and cannot be answered without knowing it.

If you have a situation that is guaranteed to match a pair of types intrinsically (semantically related in the context) you might be better off with std::pair to reflect those semantics at the API.

If you're not sure, rather use a std::tuple to keep on for future flexibility of your API design.

Another way is simply to use your own structs that clearly have named semantics for the values like:

 template<typename T>
 struct point2D {
      T x;
      T y;
 };

 template<typename T>
 struct point3D {
      T x;
      T y;
      T z;
 };

 template<typename Key, typename Value>
 struct Item {
     Key key:
     Value value;
 };

rather than std::tuple<T,T>, std::pair<T,T> or std::tuple<T,T,T>

1
Bauss On

Tuples are meant for wrapping around multiple values that may or may not have any relations to each other.

Pairs are meant for wrapping around two pairs of values that has a relation between each other ex. a key-value pair would have two values, one being a key and the other being a value, where they both have a relation to each other ex. in a hashmap where the key points to either the pair or the value.

It really comes down in how you want everything to work etc.

4
ArmenB On

I've commonly seen proper API calls or libraries (math related) return a struct or class even if the container has two values.

But if you must, I agree with @Bauss, if the value is a key, value pair then use std::pair, for example "table id, string name".

But if the tuple is not key value but both are actual values for example a position is XY coordinate (int x, int y) use std::tuple.

8
Richard Hodges On

Warning: some of what follows is opinion:

In computer science, there are only three interesting numbers:

  • zero
  • one
  • N

Because we're either dealing with the absence of something, one thing or some number of things. For example average(a0 , a1) is a special case of average(a0, ...aI) where I is every integer in the set (0 <= I < N).

It follows that std::pair is logically specialisation of an N-tuple and is therefore un-needed.

Worse, using a pair assumes that the number of items will always be 2, and bakes that into the interface. This is obviously an error, since we're trading the ability to make future nonbreaking changes for zero gain.

When the STL was first written there were no variadic templates and there were only three use-cases for pairs :-

  1. as a range resulting from equal_range and the like

  2. as an iterator-with-state returned from map::insert and,

  3. as a key/value pair in a map.

Then boost came along with its modern ideas like tuples. Being an unofficial library, boost was able to simulate variadic templates with pages and pages of auto-generated overloads. Something that would be unthinkable in a standardised API that ships with every compiler.

Now we have variadic templates. Except where unfortunately baked into the interfaces of maps, there is no reason whatsoever to prefer a pair over a tuple