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 forstd::tuple<int, std::string>
cannot exist in this circumstance because the argument ofget
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 astd::vector
that contains a mix ofint
andstd::string
, you can totally have astd::vector<Widget*>
whereIntWidget
contains anint
andStringWidget
contains astd::string
as long as both derive fromWidget
. 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.