Why are C++ tuples so weird?
Asked Answered
A

4

7

I usually create custom structs when grouping values of different types together. This is usually fine, and I personally find the named member access easier to read, but I wanted to create a more general purpose API. Having used tuples extensively in other languages I wanted to return values of type std::tuple but have found them much uglier to use in C++ than in other languages.

What engineering decisions went into making element access use an integer valued template parameter for get as follows?

#include <iostream>
#include <tuple>

using namespace std;

int main()
{
    auto t = make_tuple(1.0, "Two", 3);
    cout << "(" << get<0>(t) << ", " 
                << get<1>(t) << ", " 
                << get<2>(t) << ")\n";
}

Instead of something simple like the following?

t.get(0)

or

get(t,0)

What is the advantage? I only see problems in that:

  • It looks very strange using the template parameter like that. I know that the template language is Turing complete and all that but still...
  • 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.

Edit: I've accepted an answer. Now that I've thought about what needs to be known by the language and when it needs to be known I see it does make sense.

Acuate answered 8/7, 2019 at 15:9 Comment(11)
" indexing by runtime". That is the point, type should be known at compile time, so you cannot use runtime value as index.Excreta
Not a reason for why it has to be a template function but here is the reason it wasn't made a member function: #3313979Ali
with your get how can you have a dedicated return type depending on a computed index ? In a collection you have one type, not with a tupleBrucine
@Excreta Isn't that a major disadvantage though? Why use that mechanism for implementing them if it imposes such a severe restriction on their use?Acuate
"It looks very strange using the type parameter like that." - it's not a type parameter. Template parameters can be compile-time integral values instead of types (and are for get<N>).Cornetcy
@Acuate The "problem" is not the implementation mechanism but the typing rules of the language. C++ is (in this context) a statically typed language. What type would t.get(rand(3)) have at compile-time?Yogi
@Acuate Because it is the only way in C++ to get a compile time known value into a function. Function parameters in C++ are never compile time constants.Ali
@TonyDelroy Yes sorry I should have said template parameter.Acuate
FWIW, there is a paper that if adopted, could get you the syntax you want: open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1045r0.htmlAli
Note that std::get handles std::pair, std::array, std::tuple and std::variant transparently. This can be beneficial for generic code, allowing the type of the set to change without requiring special treatment.Subalpine
@DuncanACoulter. Why use that mechanism for implementing them if it imposes such a severe restriction on their use? No that's the point. If you decide at runtime to accesses element 2 then you are deciding at runtime the type of element you are interacting with (as each index can be a different type). C++ is strongly typed so you can not have a situation were the type is not known until runtime. All type information must be resolved at compile time (as this is info is thrown away before the application is run). If you want to each index to be the same type use std::vector!Peculiarity
P
14

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.

Phina answered 8/7, 2019 at 15:14 Comment(5)
Unless I'm mistaken, this is about static typing, not strong typing. Python is also strongly (but not statically) typed. That's why you can get type errors in Python at run-time that would've been caught at compilation time in (equivalent) C++, but also why tuple access is less restricted in Python. See en.wikipedia.org/wiki/Type_system#Type_checkingYogi
So strongly typed languages that have some common base type like Object could get away with returning that but because C++ doesn't it needs to know the exact type when you call get?Acuate
@Acuate If you have a common polymorphic base type, you can use something like std::vector<std::unique_ptr<Base>> to get what you want. But that only works if every type your container can store is a Base or Base derived type. If there is even a single other type, you have to fall back to std::vector<std::variant<[supported types]>> or std::vector<std::any> instead. Edit : std::variant constrains how you can structure your code and may help illustrate why std::get is the way that it is.Subalpine
@FrançoisAndrieux Okay in my case that is the case. Thanks for that.Acuate
I've accepted the answer. Now that I've thought about what needs to be known by the language and when it needs to be known I see it does make sense.Acuate
Y
6

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.

Yogi answered 8/7, 2019 at 15:31 Comment(2)
+1 for bringing up RTTI. I will try not to be offended by your recommendation of the type systems wikipedia page but then again I was being dense so I suppose it is fair.Acuate
To be accurate, runtime polymorphism works fine without RTTI though, as long as you don't also use things like dynamic_cast.Resale
P
3
  • 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.

Pejsach answered 8/7, 2019 at 16:15 Comment(1)
I know that strangeness is subjective and a weak argument. It was more a commentary when compared with other containers. However I see that other statically typed tuples like those in Scala are also accessed via "strange" compile type visible members like ._1 ._2 etc. so it does make sense on reflection. +1 for mentioning std::variant though I won't be using it.Acuate
B
2

Note that in C++17 you can use structured binding to make this much more obvious:

#include <iostream>
#include <tuple>

using namespace std;

int main()
{
    auto t = make_tuple(1.0, "Two", 3);
    const auto& [one, two, three] = t;
    cout << "(" << one << ", " 
                << two << ", " 
                << three << ")\n";
}
Bracteate answered 5/10, 2020 at 3:21 Comment(2)
I am going to steal this. Haha! Nice solution. =)Irreducible
I now use structured binding a lot. +1 for a nice option.Acuate

© 2022 - 2024 — McMap. All rights reserved.