When I use std::pair or std::tuple I always feel sad about using std::get<> to get results. Because this is not so readable, not so maintainable and a bit ugly. Even when you use first/second it’s easy to forget what was first argument of std::pair and so on. When you use tuple to pass more than 3 arguments situation becomes even worse. New standard has nothing in box to solve this, but we can fix this writing our own solution using C++14.
At MeetingCPP 2015 conference I attended nice presentation by Manu Sánchez and found great solution there – named tuple. After some playtime I made my own implementation draft which you can find in this post.
Ok, once again – what’s else wrong with std::tuple?
Look at standard example from cppreference.com:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
std::tuple<double, char, std::string> get_student(int id) { if (id == 0) return std::make_tuple(3.8, 'A', "Lisa Simpson"); if (id == 1) return std::make_tuple(2.9, 'C', "Milhouse Van Houten"); if (id == 2) return std::make_tuple(1.7, 'D', "Ralph Wiggum"); throw std::invalid_argument("id"); } int main() { auto student0 = get_student(0); std::cout << "ID: 0, " << "GPA: " << std::get<0>(student0) << ", " << "grade: " << std::get<1>(student0) << ", " << "name: " << std::get<2>(student0) << '\n'; } |
What if You by mistake switch ‘GPA’ and ‘grade’ inside your function? Nothing will happen to inform You that something went wrong. Information will still be passed to std::make_tuple, and auto-converted to std::cout. Except the output will be totally wrong… Is there some way to avoid this situation? A lot of you now are thinking about making custom struct with named fields, than and passing typed result as this struct. This is good solution and there are a lot of cases when this will be actually better, but … this is not so simple.
But what if you know that this temporary structure is rather limited in size and will be used just ‘here’ to pass results (and so creating of additional class to pass result will be overhead) – there is nice solution from python – named tuple.
C++14 is powerful enough to make our own named tuple. The only problem is that final syntax could be different depending on our own choice. I have chosen the following:
1 2 3 4 |
auto student0 = make_named_tuple(param("GPA") = 3.8, param("grade") = 'A', param("name") = "Lisa Simpson"); auto gpa = student0[param("GPA")]; auto grade = student0[param("grade")]; |
Creation: Instead of make_tuple() here we have make_named_tuple() and param() is creating named parameter which is set in place.
Access: To access data I use square bracers and same named parameter inside.
There are a lot of other ways to declare this – you are free to use your own syntax. It will be better then std::get<> anyway.
IMPLEMENTATION of tagged tuple
The main trick here is compile time string hash. It does not matter which specific implementation You choose for it – I just grabbed the one from Manu’s gist:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
namespace foonathan { namespace string_id { namespace detail { using hash_type = std::uint64_t; constexpr hash_type fnv_basis = 14695981039346656037ull; constexpr hash_type fnv_prime = 109951162821ull; // FNV-1a 64 bit hash constexpr hash_type sid_hash(const char *str, hash_type hash = fnv_basis) noexcept { return *str ? sid_hash(str + 1, (hash ^ *str) * fnv_prime) : hash; } } } } // foonathan::string_id::detail |
Next is simple class for named param:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
/// Named parameter (could be empty!) template <typename Hash, typename... Ts> struct named_param : public std::tuple<std::decay_t<Ts>...> { using hash = Hash; ///< key named_param(Ts&&... ts) : std::tuple<std::decay_t<Ts>...>(std::forward<Ts>(ts)...){ }; ///< constructor template <typename P> named_param<Hash,P> operator=(P&& p){ return named_param<Hash,P>(std::forward<P>(p)); }; }; template <typename Hash> using make_named_param = named_param<Hash>; |
Nothing special as You could see – i just store optional value inside inner tuple. So parameter could be empty and be used as static search key or it can contain some value and be passed to tuple construction function.
Now main tuple struct. To make it work with Visual Studio’s compiler I came up with this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
/// Named tuple is just tuple of named params template <typename... Params> struct named_tuple : public std::tuple<Params...> { template <typename... Args> named_tuple(Args&&... args) : std::tuple<Args...>(std::forward<Args>(args)...) {} static const std::size_t error = -1; template<std::size_t I = 0, typename Hash> constexpr typename std::enable_if<I == sizeof...(Params), const std::size_t>::type static get_element_index() { return error; } template<std::size_t I = 0, typename Hash> constexpr typename std::enable_if<I < sizeof...(Params), const std::size_t>::type static get_element_index() { using elementType = typename std::tuple_element<I, std::tuple<Params...>>::type; return (std::is_same<typename elementType::hash, Hash>::value) ? I : get_element_index<I + 1, Hash>(); } template<typename Hash> const auto& get() const { constexpr std::size_t index = get_element_index<0, Hash>(); static_assert((index != error), "Wrong named tuple key"); auto& param = (std::get< index >(static_cast<const std::tuple<Params...>&>(*this))); return std::get<0>( param ); } template<typename NP> const auto& operator[](NP&& param) { return get<typename NP::hash>(); } }; |
And finally make_named_tuple() and param():
1 2 3 4 5 6 7 |
template <typename... Args> auto make_named_tuple(Args&&... args) { return named_tuple<Args...>(std::forward<Args>(args)...); } #define param(x) make_named_param< std::integral_constant<foonathan::string_id::detail::hash_type, foonathan::string_id::detail::sid_hash(x)> >{} |
Yes, that’s all!
CONCLUSION
So we now have rather compact way to create named tuples in C++. This has to improve readability and simplicity of any code dealing with std::tuples and std::pairs.
And what is even more important – this scheme is more error proof. When you change places of arguments in tuple – nothing happens – everything will still work. If you make mistake in parameter name – you will get compile error right away. If during project lifetime You decide to refactor your tuple structure you will get all places where you need to change access functions, because until you do this compiler will not produce any executable. Sweet.
Source code: gist