tuples, hardest to get used to so far

Pages: 12
What did you guys think of tuple syntax when you first saw them? Did you wish the syntax was different or have you grown to understand the syntax and love them now? I know what this code does, it is the syntax that is just so disturbing.

I would imagine you see tuples often in code.

I need to get this off my chest but correct me if I am mistaken in thinking the syntax should be better? But the syntax and function calls are all over the place!

1st lines are the original and 2nd line is what I would change them to.

1
2
const int numMembers = tuple_size<tupleType>::value;
const int numMembers = tuple_size<tupleType>::size;  //NON-WORKING THEORETICAL 

Of course I would have loved it even better if it was not a helper class and just a function within the tuple class even more.... "tuple_size<tupleType>"


1
2
cout << "Last element: " << get<numMembers - 1>(tup) << endl;
cout << "Last element: " << tup[numMembers - 1] << endl;//NON-WORKING THEORETICAL 

Consistent with arrays and containers and much cleaner with [] operator.


1
2
make_tuple(101, 's', "Hello Tuple!")
tuple_make(101, 's', "Hello Tuple!")  //NON-WORKING THEORETICAL 

Keep the order consistent. And the single word "get" from previous lines could have been "tuple_get" at the very least.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>
#include <tuple>
using namespace std;

template <typename tupleType>
void DisplayTupleInfo(tupleType& tup)
{
	const int numMembers = tuple_size<tupleType>::value;
	cout << "Num elements in tuple: " << numMembers << endl;
	cout << "Last element value: " << get<numMembers - 1>(tup) << endl;
	cout << "********************\n";
}
int main()
{
	tuple<int, char, string> tup1(make_tuple(101, 's', "Hello Tuple!"));
	DisplayTupleInfo(tup1);

	auto tup2(make_tuple(3.14, false));
	DisplayTupleInfo(tup2);

	auto concatTup(tuple_cat(tup2, tup1));	//concatenate tup2 & tup1 members
	DisplayTupleInfo(concatTup);

	double pi;
	string sentence;
	tie(pi, ignore, ignore, ignore, sentence) = concatTup;//Brings out values	
	cout << "Unpack: Pi: " << pi << " and \"" << sentence << "\"" << endl;

	return 0;
}
Anything new in C/C++ is gonna look kinda weird at first, especially for a beginner. There are reasons why the particular syntax was chosen, though it might not be so obvious at first glance.

Don't bother asking me about the reason(s) for the syntax, I don't have a clue why, m'ok? Me self-taught C/C++ hobbyist.

std::tuple is a generalization of std::pair.

Structured binding can be helpful to unpack tuples.
https://en.cppreference.com/w/cpp/language/structured_binding (see case 2).
Code snippet does not mention anything using the keyword "binding," but it does say the tuple gets unpacked and copied in line 26...which I guess is the binding.

George, you know so much and are ready to write your C++ book. If you know so much and are not pursuing a C++ job, then what hope is it for a newb? How do newbs get started in this industry nowadays?

At least with this line, I could convince myself that they wanted the tuple to stand out (from the arrays and vectors). I would have been fine with just putting the prefix "tup..." on all my names to stand out "tupMyTuple" though and not more new syntax and variation.
But still when you add tons of lines of coding together with everything else it can get overwhelming.

1
2
cout << "Last element: " << get<numMembers - 1>(tup) << endl;
cout << "Last element: " << tup[numMembers - 1] << endl;//NON-WORKING THEORETICAL 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <tuple>

int main()
{
    // class template argument deduction
    // https://en.cppreference.com/w/cpp/language/class_template_argument_deduction
    std::tuple tup { 1, 2.3, 'a' } ; // deduced: std::tuple<int,double,char>

    // structured binding declaration
    // https://en.cppreference.com/w/cpp/language/structured_binding
    const auto [i,d,c] = tup ; // const int i == 1, const double d == 2.3, const char c == 'a'

    std::cout << i << ' ' << d << ' ' << c << '\n' ;
}

http://coliru.stacked-crooked.com/a/24c978404b2654ee
I would imagine you see tuples often in code. I need to get this off my chest but correct me if I am mistaken in thinking the syntax should be better? But the syntax and function calls are all over the place!

I have to admit that I don't use tuples very often. I think they are mostly useful in templated code when you need to store a bunch of values that you don't really know what they represent (e.g. if you want to store the arguments of a variadic function template in order to pass them to a function at a later time such as std::function might do internally). In such codes the use of tuple would probably not be the only thing that makes the code look complicated.

In normal situations I would just use a struct/class with named members if possible.


1st lines are the original and 2nd line is what I would change them to.
1
2
const int numMembers = tuple_size<tupleType>::value;
const int numMembers = tuple_size<tupleType>::size;  //NON-WORKING THEORETICAL  

It's a common convention that the standard uses. Look at the type traits.

https://en.cppreference.com/w/cpp/header/type_traits

If the class template is used to compute a type then it has a member named type.
If the class template is used to compute a value then it has a member named value.

Your preferred approach would lead to a lot of word repetition.

1
2
tuple_size<tupleType>::size
      ~~~~             ~~~~

And it's arguably a bit misleading too because the member does not give us the size of the tuple_size<tupleType> object.


Of course I would have loved it even better if it was not a helper class and just a function within the tuple class even more....

Despite the name, tuple_size is not just for std::tuples. You can use it on std::pair and std::array too. And if you create your own type that contains a fixed number of elements you can specialize it for that type also. This allows you to treat all tuple-like objects the same and as others have mentioned it allows you to use the type in structured bindings.

https://en.cppreference.com/w/cpp/language/structured_binding

In general the standard library prefers to keep customization points like this as non-members. This way you can add the customization to non-class types and to class types that you cannot change for whatever reason. It also avoids any name clashes with the members in the class.


1
2
cout << "Last element: " << get<numMembers - 1>(tup) << endl;
cout << "Last element: " << tup[numMembers - 1] << endl;//NON-WORKING THEORETICAL 

Don't forget that the values stored in a tuple are not necessarily of the same type. The subscript operator [] works essentially like a function (it can be overloaded). It takes a runtime value as argument and returns a runtime value of a fixed type.

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.

https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1045r1.html

Look at the before and after examples in that proposal and you'll find your suggested syntax there, so maybe we will be able to use this syntax one day, who know...


1
2
make_tuple(101, 's', "Hello Tuple!")
tuple_make(101, 's', "Hello Tuple!")  //NON-WORKING THEORETICAL  
Keep the order consistent.

When make_tuple was added we already had make_pair so calling it tuple_make would have been inconsistent.

If you mean the order should have been consistent with tuple_size I think you will have to blame the English language.

tuple_size<T> represents the "tuple size" of T. And to get its value we use ::value. It makes total sense.

make_tuple is a function template that we use to make a tuple. We don't tuple make them, do we?

I'm not saying pair_make/tuple_make would necessarily have been bad. There are pros and cons to both naming schemes. All I'm saying is that make_pair and make_tuple are the most intuitive names and what most programmers would choose unless they had been convinced the reverse is better. Since they have started to name things this way they would now need an even more convincing argument to change to a different naming scheme for new similar functions.
Last edited on
tuple's are fundamental to relational databases.
A tuple is a single row of data in a relational database table (relation).

Not exactly Oracle but ...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <iostream>
#include <vector>
#include <tuple>

int main()
{
    std::vector< std::tuple<int, std::string, char> > Student;
    std::vector< std::tuple<int, std::string> > Subject;
    std::vector< std::tuple<int, int> > Enrolment;
    
    Student.push_back( std::make_tuple(1," Bill",'A') ); // ID, NAME, INITIAL
    Student.push_back( std::make_tuple(2,"Sarah",'B') );
    Student.push_back( std::make_tuple(3,"Sally",'C') );
    
    Subject.push_back( std::make_tuple(345,"Science") ); // CODE, COURSE
    Subject.push_back( std::make_tuple(201,"History") );
    Subject.push_back( std::make_tuple(387,"  Music") );
    
    Enrolment.push_back( std::make_tuple(1,345) ); // ID, CODE
    Enrolment.push_back( std::make_tuple(2,345) );
    Enrolment.push_back( std::make_tuple(3,201) );
    Enrolment.push_back( std::make_tuple(3,345) );
    Enrolment.push_back( std::make_tuple(2,201) );
    Enrolment.push_back( std::make_tuple(1,201) );
    Enrolment.push_back( std::make_tuple(2,387) );
    
   
    int studentID{0};
    int subjectCode{0};
    
    for(auto i: Enrolment)
    {
        studentID = std::get<0>(i);
        subjectCode = std::get<1>(i);
        
        for(auto j: Student)
        {
            if( std::get<0>(j) == studentID)
                std::cout << std::get<1>(j) << ' ' << std::get<2>(j) << ' ';
        }
        
        for(auto k: Subject)
        {
            if( std::get<0>(k) == subjectCode)
                std::cout << std::get<1>(k) << '\n';
        }
    }
    return 0;
}


  Bill A Science
Sarah B Science
Sally C History
Sally C Science
Sarah B History
 Bill A History
Sarah B   Music
Program ended with exit code: 0
Last edited on
1
2
3
4
5
6
7
8
9
Student    = ( ( 1, "Bill", 'A' ), ( 2, "Sarah", 'B' ), ( 3, "Sally", 'C' ) )
Subject    = ( ( 345, "Science" ), ( 201, "History" ), ( 387, "Music" ) )
Enrollment = ( ( 1, 345 ), ( 2, 345 ), ( 3, 201 ), ( 3, 345 ), ( 2, 201 ), ( 1, 201 ), ( 2, 387 ) )

for e in Enrollment:
   for st in Student:
      if st[0] == e[0]: print( st[1], st[2], end=' ' )
   for sub in Subject:
      if sub[0] == e[1]: print( sub[1] )


Bill A Science
Sarah B Science
Sally C History
Sally C Science
Sarah B History
Bill A History
Sarah B Music


https://onlinegdb.com/p32a7dv18
Fortran?
Last edited on
Parseltongue !!
Python - where tuples are a staple of the language. (Although that code would be better written in lists, rather than tuples, since the former is mutable and the latter not.)

I think the C++ implementation of tuples is very clunky, and prefer a well-defined struct which is easy to initialise and reference. Fortran would have to use a user-defined type (similar to a C/C++ struct or class).

I'm not sure that tuples should be a priority thing for @protoseepp to study. C++ has much better elements.
Last edited on
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <tuple>
#include <iostream>
#include <string>

using Stud = std::tuple<int, std::string, char>;
using Sub = std::tuple<int, std::string >;
using Enrol = std::tuple<int, int>;

int main() {
	const Stud Student[] {{1, "Bill", 'A'}, {2, "Sarah", 'B'}, {3, "Sally", 'C'}};
	const Sub Subject[] {{345, "Science"}, {201, "History"}, {387, "Music"}};
	const Enrol Enrollment[] {{1, 345}, {2, 345}, {3, 201}, {3, 345}, {2, 201}, {1, 201},{2, 387}};

	for (const auto& [eid, esub] : Enrollment) {
		for (const auto& [sid, sname, c] : Student)
			if (eid == sid) {
				std::cout << sname << ' ' << c;
				for (const auto& [ssub, ename] : Subject)
					if (esub == ssub)
						std::cout << ' ' << ename;
			}
		std::cout << '\n';
	}
}



Bill A Science
Sarah B Science
Sally C History
Sally C Science
Sarah B History
Bill A History
Sarah B Music


But in this case could have just as easily used structs without any change to main()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>
#include <string>

struct Stud {
	int i;
	std::string s;
	char c;
};

struct Sub {
	int i;
	std::string s;
};

struct Enrol {
	int i;
	int ii;
};

int main() {
	const Stud Student[] {{1, "Bill", 'A'}, {2, "Sarah", 'B'}, {3, "Sally", 'C'}};
	const Sub Subject[] {{345, "Science"}, {201, "History"}, {387, "Music"}};
	const Enrol Enrollment[] {{1, 345}, {2, 345}, {3, 201}, {3, 345}, {2, 201}, {1, 201},{2, 387}};

	for (const auto& [eid, esub] : Enrollment) {
		for (const auto& [sid, sname, c] : Student)
			if (eid == sid) {
				std::cout << sname << ' ' << c;
				for (const auto& [ssub, ename] : Subject)
					if (esub == ssub)
						std::cout << ' ' << ename;
			}
		std::cout << '\n';
	}
}

Last edited on
Take your pick ...

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int main()
{
    using Stud = std::tuple<int, std::string, char>;
    typedef std::tuple<int, std::string> Subj;
    typedef std::tuple<int, int> Enro;
    
    std::vector< Enro > Enrolment;
    
    std::vector< Stud > Student
    = {( Stud(1," Bill",'A') ), {2,"Sarah",'B'}, Stud(3,"Sally",'C') };
    
    std::vector< Subj > Subject
    = {(Subj(345,"Science") ), Subj(201,"History"), Subj(387,"  Music") };
    
    Enrolment.push_back( std::make_tuple(1,345) ); // ID, CODE
    Enrolment.push_back( std::make_tuple(2,345) );
    Enrolment.push_back( std::make_tuple(3,201) );
    Enrolment.push_back( std::make_tuple(3,345) );
    Enrolment.push_back( std::make_tuple(2,201) );
    Enrolment.push_back( std::make_tuple(1,201) );
    Enrolment.push_back( std::make_tuple(2,387) );

   ...
A current C++ problem with template argument deduction is that it works fine for one value but not for an array/vector etc when multi value initialized. eg

 
std::tuple onearg {1, "foobar", 3.4};


compiles OK. But:

 
std::tuple twoearg[] {{1, "foobar", 3.4}, {2, "qwerty", 4.5}};


fails! Ahhh....
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <iostream>
#include <vector>
#include <tuple>

int main()
{
    using Stud = std::tuple<int, std::string, char>;
//    typedef std::tuple<int, std::string> Subj;
    typedef std::tuple<int, int> Enro;
    
    std::vector< Enro > Enrolment;
    
    std::vector< Stud > Student
    = {{1," Bill",'A'}, {2,"Sarah",'B'}, {3,"Sally",'C'} };
    
    std::vector< std::tuple<int, std::string> > Subject
    = {{345,"Science"}, {201,"History"}, {387,"  Music"} };
    
    Enrolment.push_back( std::make_tuple(1,345) ); // ID, CODE
    Enrolment.push_back( std::make_tuple(2,345) );
    Enrolment.push_back( std::make_tuple(3,201) );
    Enrolment.push_back( std::make_tuple(3,345) );
    Enrolment.push_back( std::make_tuple(2,201) );
    Enrolment.push_back( std::make_tuple(1,201) );
    Enrolment.push_back( std::make_tuple(2,387) );
    
   
    int studentID{0};
    int subjectCode{0};
    
    for(auto i: Enrolment)
    {
        studentID = std::get<0>(i);
        subjectCode = std::get<1>(i);
        
        for(auto j: Student)
        {
            if( std::get<0>(j) == studentID)
                std::cout << std::get<1>(j) << ' ' << std::get<2>(j) << ' ';
        }
        
        for(auto k: Subject)
        {
            if( std::get<0>(k) == subjectCode)
                std::cout << std::get<1>(k) << '\n';
        }
    }
    return 0;
}


 Bill A Science
Sarah B Science
Sally C History
Sally C Science
Sarah B History
 Bill A History
Sarah B   Music
Program ended with exit code: 0


https://coliru.stacked-crooked.com/a/7ff864acc6dbd50e
Last edited on
Thanks.

I had memorized the snippet after the post and I am able to parrot it back, but there is just so much more than what is on the surface of my posted code for me to look at.

::value and ::type for the returns turns out to be a good approach after all and I can more easily accept this format now as it is very consistent. Wanting to re-use code in classes or structs rather than as members makes sense too.

Oh yes, I see the proposal for my way with [] subscript and I hope they can pull it off. Although, newcomers still have to tackle and know the original way because they may encounter it in code. It would have been nice for them to pull it off from the start and never used the original format...just my opinion.

x in tup[x] sounds like then the tuple is not in some list either, so how is the tuple handled internally? Do the tuples just become/behave as if they are actual variables/members of the templatized function/class/struct? Do they just get written in during compile time into the function/object?

I would have thought it could work something like this:
if compiler sees [], then it checks if left operand is a tuple, and if so then it just rewrites this:
 
tup[numMembers - 1]


Into this...and with no run-time resources taken...am I asking for too much? This should have been done from the beginning and just masked from us.
 
get<numMembers - 1>(tup)



I have not seen any relational database yet, but I can see how tuples are befitting here. Also, good to see struct full usage vs tuple for comparison purposes, pretty much what I thought it would look like.
Don't bother asking me about the reason(s) for the syntax, I don't have a clue why, m'ok


May I refer to:

https://en.wikipedia.org/wiki/Bactrian_camel#/media/File:Camel_Farm_in_Mongolia_02.jpg

which IMO explains much...
I have not found a good use case for tuples. Its like you can make a struct/class with different syntax, OK, but you can't seem to do as much with them after creating them.
Worse is the .first .second idea, where instead of useful names, you have generic access, making code unreadable if you use them outside of tiny snippets or without a helper enum.
I sort of get how they could be a little useful for a database query result, but even so a container of variant seems cleaner.

It feels like one of two things... either eggheaderry from the theory guys or keeping up with the jones (python gots tuples, we need tuples too!). Neither one is a great way to make a decision to add it to the language. Ill admit that maybe its just the work I do that doesn't need the things and I could be missing some magical scenario where these things make a difficult problem simple. Its possible. But so far, not impressed.
x in tup[x] sounds like then the tuple is not in some list either

Just so that we don't misunderstand each other... You wrote tup[numMembers-1] but I didn't feel like writing numMembers-1 so I just used x as a placeholder for whatever expression that you pass to the subscript operator.


how is the tuple handled internally? Do the tuples just become/behave as if they are actual variables/members of the templatized function/class/struct? Do they just get written in during compile time into the function/object?

The standard does not specify exactly how it should be implemented but in practice a tuple is essentially an object that stores the values as member variables.

For example, std::tuple<int, std::string, double> is normally stored the same way in memory as:
1
2
3
4
5
6
7
8
struct TupleIntStrDbl
{
private:
	int m1;
	std::string m2;
	double m3;
    ...
};


I would have thought it could work something like this:
if compiler sees [], then it checks if left operand is a tuple, and if so then it just rewrites this:
 
tup[numMembers - 1]
Into this...and with no run-time resources taken...am I asking for too much? This should have been done from the beginning and just masked from us.
 
get<numMembers - 1>(tup)

std::tuple is a library feature. It does not require any special handling by the compiler (as far as I know).

The design of std::tuple was largely based on the Boost Tuple Library (a library which allowed you to use tuples in C++ long before C++11).

What you propose would require a change to the core language rules (which needs to be carefully considered since it affects not only std::tuple), or it would mean the compiler would have to handle std::tuple in some special way that is not available to regular C++ programmers. I think the latter is generally something to avoid because it can be confusing and people might want to do similar things for their own types. In other programming languages you often see standard types getting special treatment but this is C++ and we want to be able to write as nice (if not nicer) things ourselves.
Last edited on
Tuples are extremely handy if one is writing generic code. Generic code that works correctly on all kinds of structs is a lot messier than generic code that work on tuples. For example, to store and forward arguments to a generic callable object. Ergo std::apply is written in terms of a tuple of arguments https://en.cppreference.com/w/cpp/utility/apply

Boost.Fusion:
Fusion is a library for working with heterogeneous collections of data, commonly referred to as tuples.
...
Tuples are powerful beasts. After having developed two significant projects (Spirit and Phoenix) that relied heavily metaprogramming, it became apparent that tuples are a powerful means to simplify otherwise tricky tasks; especially those that require a combination of metaprogramming and manipulation of heterogeneous data types with values. While MPL is an extremely powerful metaprogramming tool, MPL focuses on type manipulation only. Ultimately, you'll have to map these types to real values to make them useful in the runtime world where all the real action takes place.

https://www.boost.org/doc/libs/1_79_0/libs/fusion/doc/html/fusion/preface.html

jonnin wrote:
Worse is the .first .second idea, where instead of useful names, you have generic access, making code unreadable if you use them outside of tiny snippets

I agree. I was thinking about std::map/std::unordered_map and how it would have been nicer if they stored the key-value pair in a struct with members named "key" and "value" rather than using std::pair with members named "first" and "second".

Please, people, don't say structure bindings makes this a non-issue. It helps but it's far from perfect. With structured bindings you need to type the names in the correct order which you can easily get wrong.
Last edited on
Pages: 12