Why is tuple_size a trait and not a member

1.1k views Asked by At

Why is tuple_size a free trait and not a member variables/typedefs within the class? The latter has a much smaller risk of causing ODR violations.

Is there a concrete use case where having a trait tuple_size is better than defining this inline within the class? Is there a motivating case similar to that of iterator_traits (https://stackoverflow.com/a/6742025/5501675) The only one I see is the benefit it brings with incomplete types. Though not entirely sure what a use case for that is

tuple_size could easily be made a member something like this

class Something {
public:
    static constexpr auto tuple_size = 3;
};

And then the free implementation of the tuple_size trait can be

template <typename T, EnableIfHasTupleSize<std::decay_t<T>>* = nullptr>
struct tuple_size 
    : public std::integral_constant<std::size_t, 
                                    std::decay_t<T>::tuple_size> 
{};

Such an implementation would bring both the benefits of having a tuple_size that can work with incomplete types if specialized as well as offering the security of the member constant where needed. Similar to iterator_traits


This example was given to me by @T.C. recently of how tuple_size can easily cause ODR violations.

struct X { int a; };

template<> struct std::tuple_size<X>;
template<size_t I> struct std::tuple_element<I, X> { using type = int; };
template<size_t I> int get(X) { return 1; }


call_func_that_checks_value_of_tuple_size(X{});

template<> struct std::tuple_size<X> : std::integral_constant<size_t, 3> {};

call_func_that_checks_value_of_tuple_size(X{});

Compilers are able to pick up on this, however if this is split between translation units, its very hard for a compiler to catch this.

3

There are 3 answers

0
Yakk - Adam Nevraumont On

In C++17 structured bindings require that a type support std::tuple_size<T>, ADL-enabled get<std::size_t>, and std::tuple_element<std::size_t, T>.

If a type supports all 3, structured binding works.

This permits you to take a type provided by a 3rd party, like QPair, and without modifying their source code or requiring them to update, use it in a structured binding way in your code.

If tuple_size was a member of tuple (and array and pair), then we would have had to add a tuple_size trait in C++17 to have this capability anyhow.

0
NoSenseEtAl On

One possible reason is symmetry with std::get (that can not easily be member because glorious C++ grammar).

0
Germán Diago On

std::tuple was implemented in the technical report 1 before entering the standard.

This was before C++11 existed, hence, no constexpr functions. Since std::tuple_size is a constant value, it made sense to make it a template at the time to make it a constant expression (it could perfectly be a .size() constexpr member AFAIK with no problem).

As for the non-member, std::array also supports std::tuple_size and any struct could support it in theory.

So I think these are the potential reasons for the decision but I do not know the real decisions behind it, I just reasoned it.