Depending on their use,
std::tuple can be code smells. That’s why we should be careful around these two.
Having a code smell is not a no-go, it’s more like a red flag. It’s one of those things that are not a problem themselves but rather a hint that there might be a less obvious problem hidden in the code.
The “Data Class” smell
In object orientation, there is a code smell named “Data Class”. It says that having a class that does not contain any logic is a hint at a violation of design principles.
std::tuple may or may not constitute the “Data Class” smell, because C++ is not an object oriented language. However, if we find them used in an object oriented context, we should definitely have a closer look.
Cohesion and Coupling
In software, we usually want things that belong together to have high cohesion. It means that all the code that deals with the two things as a conceptually whole should be closely related to them. Usually, there is some logic associated with the data, that specifies how the values relate to each other. Things, that are not closely related should, on the other hand, be loosely coupled, i.e. they should not travel in packs.
These are the principles that might be violated when we see the “Data Class” smell. Usually, there is some logic that belongs to the data, but it’s implemented elsewhere where it does not belong. In the case of
tuple, we can not add logic to the class, so when there is more than just a source and a consumer for the data structure, we should definitely consider to refactor it to a proper class. If, on the other hand, the data just happens to be found together by accident, tying it up into a common data structure should be suspicious.
tuple are very generic by design. Good names, however, transport a lot of information for readers of our code. Reading
std::pair<bool, iterator> does not tell us anything except that there are some boolean value and an iterator crammed together in a single data structure. If on the other hand, we had the name
InsertionResult, we would have an idea where those values came from.
The same goes for the access to the single members.
tuple tell us something about the position of the data we access, but nothing about their semantics. With named members, we do not even have to know the position, and that’s a good thing. The less we have to memorize such details, the more we can concentrate on things that really matter.
By the way, the
insert methods of
std::set don’t really return a
std::pair<bool, iterator> – it’s a
std::pair<iterator, bool>. My condolences if you spotted that without looking it up – that means you have memorized information the library could give you in a much handier way. I’d prefer to see members
position in a
Since I am picking on
std::map already: I’d sometimes also like to have
map<K,V>::value_type be something else than a
pair<const K, V>. Here, the position is much more intuitive than in the result of
insert. Still, members named
mapped would be more consistent to the
mapped_type than the generic
Having said that, I consider this a gray zone in the case of the standard library.
std::pair are equally generic, and the values are usually not passed around too much but quickly consumed.
Unless it’s a short-lived, purely technical solution with little or no associated logic, we should be wary of uses of
std::tuple. Way too often the two just are a sign of laziness because the developer who introduced them did not want to introduce a small class that bundles well-named data with the associated logic.