Why are C++ tuples so weird?

The second you've said:

It makes indexing by runtime generated indices difficult (for example for a small finite ranged index I've seen code using switch statements for each possibility) or impossible if the range is too large.

C++ is a strongly static typed language and has to decide the involved type compile-time

So a function as

template <typename ... Ts>
auto foo (std::tuple<Ts...> const & t, std::size_t index)
 { return get(t, index); }

isn't acceptable because the returned type depends from the run-time value index.

Solution adopted: pass the index value as compile time value, so as template parameter.

As you know, I suppose, it's completely different in case of a std::array: you have a get() (the method at(), or also the operator[]) that receive a run-time index value: in std::array the value type doesn't depends from the index.


The "engineering decisions" for requiring a template argument in std::get<N> are located way deeper than you think. You are looking at the difference between static and dynamic type systems. I recommend reading https://en.wikipedia.org/wiki/Type_system, but here are a few key points:

  • In static typing, the type of a variable/expression must be known at compile-time. A get(int) method for std::tuple<int, std::string> cannot exist in this circumstance because the argument of get cannot be known at compile-time. On the other hand, since template arguments must be known at compile-time, using them in this context makes perfect sense.

  • C++ does also have dynamic typing in the form of polymorphic classes. These leverage run-time type information (RTTI), which comes with a performance overhead. The normal use case for std::tuple does not require dynamic typing and thus it doesn't allow for it, but C++ offers other tools for such a case.
    For example, while you can't have a std::vector that contains a mix of int and std::string, you can totally have a std::vector<Widget*> where IntWidget contains an int and StringWidget contains a std::string as long as both derive from Widget. Given, say,

    struct Widget {
       virtual ~Widget();
       virtual void print();
    };
    

    you can call print on every element of the vector without knowing its exact (dynamic) type.


  • It looks very strange

This is a weak argument. Looks are a subjective matter.

The function parameter list is simply not an option for a value that is needed at compile time.

  • It makes indexing by runtime generated indices difficult

Runtime generated indices are difficult regardless, because C++ is a statically typed language with no runtime reflection (or even compile time reflection for that matter). Consider following program:

std::tuple<std::vector<C>, int> tuple;
int index = get_at_runtime();
WHATTYPEISTHIS var = get(tuple, index);

What should be the return type of get(tuple, index)? What type of variable should you initialise? It cannot return a vector, since index might be 1, and it cannot return an integer, since index might be 0. The types of all variables are known at compile time in C++.

Sure, C++17 introduced std::variant, which is a potential option in this case. Tuple was introduced back in C++11, and this was not an option.

If you need runtime indexing of a tuple, you can write your own get function template that takes a tuple and a runtime index and returns a std::variant. But using a variant is not as simple as using the type directly. That is the cost of introducing runtime type into a statically typed language.