6

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.

DuncanACoulter
  • 2,095
  • 2
  • 25
  • 38
  • 14
    *" indexing by runtime"*. That is the point, type should be known at compile time, so you cannot use runtime value as index. – Jarod42 Jul 08 '19 at 15:11
  • 2
    Not a reason for why it has to be a template function but here is the reason it wasn't made a member function: https://stackoverflow.com/questions/3313479/stdtuple-get-member-function – NathanOliver Jul 08 '19 at 15:12
  • 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 tuple – bruno Jul 08 '19 at 15:13
  • @Jarod42 Isn't that a major disadvantage though? Why use that mechanism for implementing them if it imposes such a severe restriction on their use? – DuncanACoulter Jul 08 '19 at 15:13
  • 2
    *"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`). – Tony Delroy Jul 08 '19 at 15:13
  • @DuncanACoulter 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? – Max Langhof Jul 08 '19 at 15:14
  • @DuncanACoulter 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. – NathanOliver Jul 08 '19 at 15:14
  • @TonyDelroy Yes sorry I should have said template parameter. – DuncanACoulter Jul 08 '19 at 15:15
  • FWIW, there is a paper that if adopted, could get you the syntax you want: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1045r0.html – NathanOliver Jul 08 '19 at 15:23
  • 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. – François Andrieux Jul 08 '19 at 15:25
  • @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`! – Martin York Jul 08 '19 at 17:20

4 Answers4

13

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.

max66
  • 65,235
  • 10
  • 71
  • 111
  • 6
    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 https://en.wikipedia.org/wiki/Type_system#Type_checking – Max Langhof Jul 08 '19 at 15:16
  • 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? – DuncanACoulter Jul 08 '19 at 15:18
  • 1
    @DuncanACoulter If you have a common polymorphic base type, you can use something like `std::vector>` 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>` or `std::vector` instead. Edit : `std::variant` constrains how you can structure your code and may help illustrate why `std::get` is the way that it is. – François Andrieux Jul 08 '19 at 15:21
  • @FrançoisAndrieux Okay in my case that is the case. Thanks for that. – DuncanACoulter Jul 08 '19 at 15:22
  • 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. – DuncanACoulter Jul 08 '19 at 18:18
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.

Max Langhof
  • 23,383
  • 5
  • 39
  • 72
  • +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. – DuncanACoulter Jul 08 '19 at 18:16
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.

eerorika
  • 232,697
  • 12
  • 197
  • 326
  • 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. – DuncanACoulter Jul 08 '19 at 18:11
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";
}
parsley72
  • 8,449
  • 8
  • 65
  • 98