
Many programmers are familiar with the concepts of pairs and tuples (pair and tuple) - they are implemented in STL, Boost (and maybe somewhere else). For those who do not know what it is, I will briefly explain - these are templates that allow grouping several values (a pair - only 2, tuple - a lot) in order to store / transmit / receive them together.
Example from MSDN:
pair <int, double> p1 ( 10, 1.1e-2 ); pair <int, double> p2 = make_pair ( 10, 2.22e-1 ); cout << "The pair p1 is: ( " << p1.first << ", " << p1.second << " )." << endl; cout << "The pair p2 is: ( " << p2.first << ", " << p2.second << " )." << endl;
At first, the idea seems tempting, because:
- Instead of transferring several vectors of the same dimension to a function, only one vector of pairs / tuples can be transferred without worrying about checking their conformity.
- You can easily return a set of values from a function without bothering with pointers or links in out-parameters (for many it’s difficult)
- It is possible to avoid creating heaps of small structures of 2-3 fields (less code is better).
But there is a dark side to this power.
As they say in the famous saying - simplicity is worse than stealing. Couples and tuples - just that case. They really give all the advantages described above. But let's think about what price.
Contents of a pair or tuple - riddle
Let's look at the announcement of such a function:
pair<string, string> GetInfo( int personId );
What do you think she returns? First and last name? And where did you get it? Maybe - passport number and tax code? And maybe the full name and phone number. Or real name and nickname. The idea is that nowhere in the declaration of the function is not described what will be contained in the returned pair. Of course, you remember that now. And remember in a year? In addition, another programmer who decides to use this function will have to crawl into its code and look for what it returns - and this really puts an end to the whole OOP approach, modularity, encapsulation, and other important things.
Compare the above code with the following:
struct Person { string Name; string Surname; } Person GetInfo( int personId );
Everything is crystal clear, there is no need to read the function code to understand the return value.
The order of the data in a pair or a tuple is a mystery
Ok, we thought well over the previous example and rewrote our function like this:
pair<string, string> GetNameAndSurname( int personId );
Now it is clearly clear that she returns the name and surname of the person. But the question is - in what order? It seems quite obvious to you that a certain order of these lines is in a pair, but I have bad news for you. You live in a world where people use different word orders in names, different date and time formats, write from left to right, and vice versa, drive along roads with versatile traffic, etc. The fact that you think the only possible way is that only this variant of the order of the values in a pair does not prove anything. As one of the laws of Murphy says - "
If something can be interpreted in several ways, it will be interpreted by the very wrong of them ."
In the case of using a separate structure (class) for the return value, we always have an unambiguous interpretation of the code.
')
Bad extensibility
Go ahead - what will happen if, over time, we want to add more data to our function? Yes, we can replace the pair with a tuple and build it up to absurd size:
tuple<string, string, int> GetNameAndSurnameAndBirthday( int personId );
But what a nightmare for all those who use this function! It turns out that after each change it needs to revise all function calls, checking whether we are accessing the correct fields. Horror.
Inability to show the absence of one of the values
Sometimes in a set of values one or more fields may not be set. It is very easy to display in a structure or class (create an isSet variable or write a field validation method), but it is absolutely impossible to display it in a pair or tuple, where it is assumed that the set contains all the values and they are valid. As a result, one has to cope with agreements in the spirit of “if the second parameter is -1, then there is actually no information”, which are not obvious, forgotten and inconvenient.
Nowhere to insert validation check
Let's look at this function, which returns the operating temperature range of a device:
pair<int, int> SomeDevice::GetCelsiusTemperatureRange() { ... return make_pair( -300, +30 ); }
As a consequence of a typo in 1 symbol, the function (without straining) expanded the boundaries of physical reality, stating that the device can operate at -300 Celsius. There is simply no check for the validity of such a temperature either at the moment of creating the object of the pair, or at the time of returning this value from the function. And there is nowhere to write it.
It is just the case if the temperature range object was returned, during the creation of which one could somehow catch an invalid value and respond to it (an assertion, a log, an exception, a replacement for a valid value, etc.)
struct TemperatureRange { int minTemp; int maxTemp; TemperatureRange( int min, int max ) { assert( min <= max ); assert( min >= -273 ); minTemp = min; maxTemp = max; } } TemperatureRange SomeDevice::GetCelsiusTemperatureRange() { ... return TemperatureRange( -300, +30 );
Counterexample
What would mean "
most often bad " in the title of the article? It must be admitted that sometimes couples can and should be used. For example, we have a game in which in the course of game mechanics for two players, we need to throw away some random values (numbers in the int range). This can be done by a function like:
pair<int, int> GetTwoRandomNumbers();
Why is this feature not bad? Everything is very simple:
- It is clearly understood what is in a pair. There is no way of ambiguous interpretation.
- The order does not matter. What is the first number for the first player, then for the second, on the contrary - on the drum.
- Our game is only for two players and never (by design) will not be possible for more - there is no need to worry about extensibility
- Both values should be exactly. The absence of one of them is impossible.
- Validation is not needed - by definition, the entire range of int suits us.
Moreover, in this example, the pair is better than a separate class (less code), better out-parameters in the form of pointers (no need to check them for validity) and better than an array or vector (they can be of any size that confuses).
In general, the example has the right to life.
Conclusion
The use of pairs or tuples seems to me to be of little justification if you are trying to write understandable, easily readable and well extensible code. The use of small classes or structures will almost always benefit in readability, except in very simple cases.