Depending on their use, std::pair and 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.

In C++, std::pair and 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 pair and 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.

Poor naming

The names pair and 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. first , second for pair and std::get<4>() for 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::map and 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 success and position in a std::map::insertion_result .

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 key and mapped would be more consistent to the key_type and mapped_type than the generic first and second .

Having said that, I consider this a gray zone in the case of the standard library. std::map and std::pair are equally generic, and the values are usually not passed around too much but quickly consumed.

Conclusion