tuples, hardest to get used to so far

Pages: 12
I went through all your relational db codes here and I understand them and can probably duplicate them now and I am sure will be helpful when I get to those related chapters.

@Peter87
That is the way I assumed tuples might work, and yes I understood your meaning behind index [x]:
For example, std::tuple<int, std::string, double> is normally stored the same way in memory as:
struct TupleIntStrDbl
{
private:
int m1;
std::string m2;
double m3;
...
};


Now, I still don't understand why the compiler can't just do that, when it can create variadic recursive functions during compile time, can do many things including take the first line below and really write out the 2nd line. But I guess I will just have to take your word for it, as you seem to know better and the fact that they just simply did not do it from the start is a big enough clue.
1
2
tuple<int, char, string> tup1(101, 's', "Hello Tuple!");
tuple<int, char, string> tup1(make_tuple(101, 's', "Hello Tuple!"));	


I thought I would understand the comments below from you a bit better on a fresher day, but I am still grappling with the "constant" reference and it might be a play on the word "constant", so let me take a step back:

If x in tup[x] is not required to be a compile time constant then how would the compiler know what type the expression returns? The answer is that it would generally be impossible and that is a big problem in a statically-typed language like C++.

If x in tup[x] was required to be a compile time constant then the rules of the language need to change. We would essentially need constexpr parameters which has been proposed but I'm not sure how that will progress.



 
cout << "Last element: " << get<numMembers - 1>(tup) << endl;

Here "get<numMembers - 1>" is NOT a constant, because I can refer to index 0,1, and 2.
get<0> --> tup[0] --> 101 //tup theoretical index reference tup[0]
get<1> --> tup[1] --> 's'
get<2> --> tup[2] --> "Hello Tuple!"

Inside DisplayTupleInfo from my original code, (tupleType& tup) is not declared as a constant, it is a reference to the three heterogenous variables. The TYPES of the variables in the arguments from main are constant though and cannot be changed. When I am inside the DisplayTupleInfo function I can get<0>. get<1>, get<2> references to the type all day long and even though the tuple is written into the object (as if it were the members) we do not loose that constant TYPE reference. The compiler somehow connects them in "tup" reference.
So now all of a sudden then I want to:
 
cout << "Last element: " << tup[numMembers - 1] << endl;//NON-WORKING THEORETICAL 

...And I lose the "tup" constant reference, where before I had it with:
 
cout << "Last element: " << get<numMembers - 1>(tup) << endl;

Why, because the tuples get written into DisplayTupleInfo as members and we lose the "constant" TYPE reference, so that we no longer know the TYPE?????? Did I get that right, or am I just not understanding????

@JLBorges
Okay, I can accept that we are showing simpler data tuple collections here, but the benefits become more apparent on larger heterogeneous collections for metaprogramming and manupulation.
Last edited on
Now, I still don't understand why the compiler can't just do that ...

It probably could (if the standard specified how it should work) but it's unusual to have such special rules for a particular library feature. It's usually better to come up with more general language features that can be used by more code otherwise you risk ending up with a very inconsistent language that is hard to update because with any new language feature that is introduced they must be very careful to not break any of these small little "hacks" that seemed like a good idea at the time.


... when it can create variadic recursive functions during compile time ...

Variadic templates is a language feature that has many use cases. It was not designed with only one particular library feature in mind.


...can do many things including take the first line below and really write out the 2nd line ...
1
2
tuple<int, char, string> tup1(101, 's', "Hello Tuple!");
tuple<int, char, string> tup1(make_tuple(101, 's', "Hello Tuple!"));	

It doesn't. There is no magic going on here. Just normal constructors and function calls.

The first line passes the arguments directly to the constructor.
The second line uses a function that will call the constructor internally and return the tuple.

The purpose of make_tuple is to be able to deduce the template arguments,
so instead of having to write
 
std::tuple<int, double, bool>(1, 2.5, true);
you can write
 
std::make_tuple(1, 2.5, true);
C++17 introduced class template argument deduction (CTAD) so now you can write
 
std::tuple(1, 2.5, true);
so I'm not sure how useful make_tuple is any more.


Here "get<numMembers - 1>" is NOT a constant, because I can refer to index 0,1, and 2.

The expression that you pass between < and > has to be a compile time constant. That means numMembers-1 needs to be a constant expression (an expression that can be evaluated to a constant value at compile time) otherwise you'll get a compilation error. This is based on language rules and is not affected by compiler optimizations and things like that.

1
2
int x = 0;
std::cout << std::get<x>(tup) // error because x is not a compile time constant. 

The reason I said compile time constant and not just constant is to make it clear it's not always enough to declare a variable with the const keyword. Sometimes people use the word runtime constant to refer to const variables that are not compile time constants.

1
2
3
4
int y;
std::cin >> y;
const int x = y;
std::cout << std::get<x>(tup) // still an error 



Inside DisplayTupleInfo from my original code, (tupleType& tup) is not declared as a constant, it is a reference to the three heterogenous variables.

Whether the tuple is constant or not doesn't matter. That's not what we're discuss here. It is the template arguments (what you write between < and >) that has to be constant. That's always the case and necessary for the way templates work.


When I am inside the DisplayTupleInfo function I can get<0>. get<1>, get<2> references to the type all day long

Note that get<0>(tup), get<1>(tup) and get<2>(tup) is essentially calling three different functions (with different return types).

Also note that std::get has additional template arguments that gets deduced from the tuple argument so calling get<0> on a std::tuple<int, double> and on std::tuple<float, bool> is technically calling two different functions. That's what allows it to have different return types (int& and float&).


even though the tuple is written into the object (as if it were the members) we do not loose that constant TYPE reference. The compiler somehow connects them in "tup" reference.

I'm having a hard time decipher this.

A tuple is an object.

std::tuple<int, double> is a different type than std::tuple<float, bool>.


So now all of a sudden then I want to:
 
cout << "Last element: " << tup[numMembers - 1] << endl;//NON-WORKING THEORETICAL 
...And I lose the "tup" constant reference, where before I had it with:
 
cout << "Last element: " << get<numMembers - 1>(tup) << endl;

According to the current language rules what's written inside [] (or ()) are not required to be constant,
but what's written inside <> always has to be constant. That's a big mismatch.

In this case you need the INDEX argument (not the tuple itself) to be constant (and it needs to be usable as a constant in the implementation) but there is currently no way for the [] operator to enforce that.
Last edited on
1) Oh, every time I saw "compile time constant" I felt they were trying to tell me something more, but I convinced myself it was the same as constant with a stress that the compiler checks and gives an error otherwise. I understand now, thanks!

2) It all makes sense now. After I read your last post, I then went and tried this:
 
cout << get<numMembers + 1>(tup) << endl;


...and bingo! Unlike the array and vector [] subscripts, Tuples actually checks to make sure that the tuple does not pass its boundaries...AND THAT IS WHY it needs a constant get<const x> there.

3)
even though the tuple is written into the object (as if it were the members) we do not lose that constant TYPE reference. The compiler somehow connects them in "tup" reference.

I'm having a hard time decipher this.

A tuple is an object.

std::tuple<int, double> is a different type than std::tuple<float, bool>.


What I was trying to say there was that, yes "tup" is the object and that the tuple variables gets written as members. And somehow "tup" knows which of those members your referencing (get<0>, get<1>...) even though they are now simply members in the object..."tup" still maintains that indexed reference connection....which is why I previously did not understand why it could just not use tup[] index reference:
1
2
3
4
5
6
7
8
struct TupleIntStrDbl
{
private:
	int m1;
	std::string m2;
	double m3;
    ...
};

I was going to argue that the compiler even does "auto" type deduction, so why couldn't it do tuple []...But now that I know that tuples do boundary checking and need that constant reference, the point is moot.

4) BTW, I could still argue the English usage in tuples. We say concatenate tuples, but we have to type "tuple_cat()". I would have much more appreciated consistency and there is something streamlined about:
tuple_concat
tuple_make
tuple_size


Thanks for your help Peter, every time I post I learn so much more and it is highly appreciated. Thanks to all that replied...my heroes!!!!!
> I would have much more appreciated consistency and there is something streamlined about:
> tuple_concat
> tuple_make
> tuple_size

We could write these functions ourselves. For example,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <tuple>

template < typename A, typename B >
concept tup_catable = requires( A a, B b ) { std::tuple_cat(a,b) ; };

template < typename A, typename B >
constexpr auto operator + ( A&& a, B&& b ) requires tup_catable<A,B>
{ return std::tuple_cat( std::forward<A>(a), std::forward<B>(b) ) ;}

template < typename A, typename B >
constexpr auto tuple_concat( A&& a, B&& b ) requires tup_catable<A,B>
{ return std::forward<A>(a) + std::forward<B>(b) ;}

int main()
{
    constexpr std::tuple a { 1, 2.3, 'a' } ;
    constexpr std::tuple b { 'b', 82LL, nullptr } ;
    constexpr auto c = a+b ;
    if constexpr( c == std::tuple{ 1, 2.3, 'a', 'b', 82LL, nullptr } ) std::cout << "ok\n" ;
    static_assert( c == tuple_concat(a,b) ) ;
}

http://coliru.stacked-crooked.com/a/30c919f821477a19
If std::tuple only was allowed to contain objects of the same type then I'm sure they would have provided a [] operator that could be indexed with a runtime value. But wait, isn't this std::array?

With std::array you have a choice if you want to pass an index that can vary at runtime to [] (unchecked) or at (throws exception if out of bounds), or if you want to use get with a constant index that leads to a compilation error if it is out of bounds.

https://en.cppreference.com/w/cpp/container/array/operator_at
https://en.cppreference.com/w/cpp/container/array/at
https://en.cppreference.com/w/cpp/container/array/get
Last edited on
Tuples are useful if you want to process different structs in a generic way. It's possible to convert (at compile time) a struct to a tuple. You can then write handlers that process these tuples in more general ways.
Topic archived. No new replies allowed.
Pages: 12