包装多个数据的元组 tuple
既然两个元素可以用 pair
来包装到一起,那么更多的元素能不能用一种类似的数据结构包装在一起呢?这便是更一般的元组 tuple
的设计初衷,可以把多个不同数据类型的数据绑定到一个实例上。比如现在我们手上有这三种数据,学生的名字、平时表现评级以及期末考试成绩,那么我们就可以选择使用 tuple
来操作我们的数据。具体而言请参考如以下代码所示。
#include <iostream>
#include <string>
#include <tuple>
#include <functional>
int main()
{
std::string names[3] = { "alice", "bob", "carl" };
char ranks[3] = { 'A', 'B', 'C' };
int score[3] = { 5, 6, 7 };
auto student0 = std::make_tuple(names[0], ranks[0], score[0]);
std::cout << "student0> name: " << std::get<0>(student0)
<< ", rank: " << std::get<1>(student0)
<< ", score: " << std::get<2>(student0) << std::endl;
std::string hoge;
char piyo;
int fuga;
auto student1 = std::make_tuple(names[1], ranks[1], score[1]);
std::tie(hoge, piyo, fuga) = student1;
std::cout << "student1> name: " << hoge << ", rank: " << piyo
<< ", score: " << fuga << std::endl;
auto student2 = std::tie(names[2], ranks[2], score[2]);
auto [a, b, c] = student2; // C++17 structured binding
std::cout << "student2> name: " << a << ", rank: " << b
<< ", score: " << c << std::endl;
return 0;
}
// filename: ch1-tuple-example-1.cpp
// compile this> g++ ch1-tuple-example-1.cpp -o ch1-tuple-example-1.exe -std=c++17
从这份代码我们不难看出,和 pair
类似,tuple
的组装与拆包任务分别由两个函数来担当,std::make_tuple()
创建一个元组,std::get()
则获取确定位置的数据。同时注意到还有一个函数 std::tie()
既可以组装元组,也可以对一个元组进行拆包。关于 std::tie()
与 std::make_tuple()
的异同,我们通过下面的例子来一探究竟。
#include <iostream>
#include <string>
#include <tuple>
#include <utility>
#include <functional>
using namespace std;
template<class T1, class T2>
ostream& operator<<(ostream &out, const pair<T1, T2> &_)
{
return out << "(" << _.first << ", " << _.second << ")";
}
void display_separator() { cout << "--------" << endl; }
int main()
{
string name = "alice";
char rank = 'A';
int score = 5;
auto student = make_tuple(name, rank, score);
// => name: alice, rank: A, score: 5
cout << "student> name: " << get<0>(student)
<< ", rank: " << get<1>(student)
<< ", score: " << get<2>(student) << endl;
cout << "Here, (score, get<2>(student)) is "
<< make_pair(score, get<2>(student)) << endl;
score += 10;
cout << " score += 10 => "
<< make_pair(score, get<2>(student)) << endl;
get<2>(student) += 100;
cout << "get<2>(student) += 100 => "
<< make_pair(score, get<2>(student)) << endl;
return 0;
}
// filename: ch1-tuple-example-2.cpp
// compile this> g++ ch1-tuple-example-2.cpp -o ch1-tuple-example-2.exe -std=c++17
这份代码的输出如下所示:
student> name: alice, rank: A, score: 5
Here, (score, get<2>(student)) is (5, 5)
score += 10 => (15, 5)
get<2>(student) += 100 => (15, 105)
在这个例子中,我们组装元组时使用的是 std::make_tuple()
方法,而获取元素则使用了 std::get()
方法。注意到我们在代码的第 30 行和第 33 行分别对外部的数值 score
和元组内部的数值 std::get<2>(student)
依次进行了变动,并且把这两个值用 std::make_pair()
方法将其值打包输出。注意到我们的输出结果里,只有变动的那一项发生了改变。这说明 std::make_tuple()
和 std::make_pair()
类似,默认新创建一个变量而且直接构建参数的一个引用。和之前类似,如果需要包装一个变量的引用,则额外使用 std::ref()
即可。
在充分了解到 std::make_tuple()
的这一性质之后,我们通过下一个例子来观察 std::tie()
在组装与拆包的表现,同时观察在 C++17 引入的 structured binding 方法与 std::tie()
在拆包时候的异同。
#include <iostream>
#include <string>
#include <tuple>
#include <utility>
#include <functional>
using namespace std;
template<class T1, class T2>
ostream& operator<<(ostream &out, const pair<T1, T2> &_)
{
return out << "(" << _.first << ", " << _.second << ")";
}
template<class T1, class T2, class T3>
ostream& operator<<(ostream &out, const tuple<T1, T2, T3> &_)
{
auto [_1, _2, _3] = _;
return out << "[" << _1 << ", " << _2 << ", " << _3 << "]";
}
void display_separator() { cout << "--------" << endl; }
int main()
{
string names[3] = { "alice", "bob", "carl" };
char ranks[3] = { 'A', 'B', 'C' };
int score[3] = { 5, 6, 7 };
int hoge;
auto student0 = tie(names[0], ranks[0], score[0]);
// => name: bob, rank: B, score: 6
tie(std::ignore, std::ignore, hoge) = student0;
cout << "student0> [hoge, score[0], get<2>(student0)] is "
<< make_tuple(hoge, score[0], get<2>(student0)) << endl;
hoge += 10;
cout << " hoge += 10 => "
<< make_tuple(hoge, score[0], get<2>(student0)) << endl;
score[0] += 100;
cout << " score[0] += 100 => "
<< make_tuple(hoge, score[0], get<2>(student0)) << endl;
get<2>(student0) += 1000;
cout << "get<2>(student0) += 1000 => "
<< make_tuple(hoge, score[0], get<2>(student0)) << endl;
display_separator();
auto student1 = make_tuple(names[1], ranks[1], score[1]);
// => name: carl, rank: C, score: 7
auto [_1, _2, piyo] = student1;
cout << "student1> [piyo, score[1], get<2>(student1)] is "
<< make_tuple(piyo, score[1], get<2>(student1)) << endl;
piyo += 10;
cout << " piyo += 10 => "
<< make_tuple(piyo, score[1], get<2>(student1)) << endl;
score[1] += 100;
cout << " score[1] += 100 => "
<< make_tuple(piyo, score[1], get<2>(student1)) << endl;
get<2>(student1) += 1000;
cout << "get<2>(student1) += 1000 => "
<< make_tuple(piyo, score[1], get<2>(student1)) << endl;
display_separator();
auto student2 = tie(names[2], ranks[2], score[2]);
// => name: carl, rank: C, score: 7
auto [_3, _4, fuga] = student2;
cout << "student2> [fuga, score[2], get<2>(student2)] is "
<< make_tuple(fuga, score[2], get<2>(student2)) << endl;
fuga += 10;
cout << " fuga += 10 => "
<< make_tuple(fuga, score[2], get<2>(student2)) << endl;
score[2] += 100;
cout << " score[2] += 100 => "
<< make_tuple(fuga, score[2], get<2>(student2)) << endl;
get<2>(student2) += 1000;
cout << "get<2>(student2) += 1000 => "
<< make_tuple(fuga, score[2], get<2>(student2)) << endl;
return 0;
}
// filename: ch1-tuple-example-3.cpp
// compile this> g++ ch1-tuple-example-3.cpp -o ch1-tuple-example-3.exe -std=c++17
这份代码的运行结果如下:
student0> [hoge, score[0], get<2>(student0)] is [5, 5, 5]
hoge += 10 => [15, 5, 5]
score[0] += 100 => [15, 105, 105]
get<2>(student0) += 1000 => [15, 1105, 1105]
--------
student1> [piyo, score[1], get<2>(student1)] is [6, 6, 6]
piyo += 10 => [16, 6, 6]
score[1] += 100 => [16, 106, 6]
get<2>(student1) += 1000 => [16, 106, 1006]
--------
student2> [fuga, score[2], get<2>(student2)] is [7, 7, 7]
fuga += 10 => [17, 17, 17]
score[2] += 100 => [117, 117, 117]
get<2>(student2) += 1000 => [1117, 1117, 1117]
下面我们逐段解析这份代码:
- 第 33 行使用了一个全新的关键字
std::ignore
,这个关键字的意思是解包时忽略它所在位置的数据,也可以理解为占位符;
- 第 30-44 行展示了第一个例子,注意到在
student0
中利用 std::tie()
解包得到的变量 hoge
并没有与另外两个变量同步,这是因为 hoge
并不是引用类型;而另外的两个变量均同步发生了变化,这说明这两个变量的其中一个是另一个的引用类型;从而可以推测出,std::tie()
会默认创建原始变量的引用,然后在包装到 tuple
里面;
- 第 48-61 行展示了第二个例子,注意到三个变量是完全独立变化的,因此可以推测出利用
auto [_1, _2, piyo]
解包出的数据可能是原原本本的复制;
- 第 65-78 行展示了第三个例子,注意到三个变量是完全同步的,原因是
std::tie()
构造了原始数据的引用,而且 auto [_3, _4, fuga]
解包时同样复制了一份引用出来。
为了验证我们的这一结论,我们接着看下一个例子
#include <iostream>
#include <string>
#include <tuple>
#include <type_traits>
#include <functional>
using namespace std;
void display_separator() { cout << "--------" << endl; }
int main()
{
string names[4] = { "alice", "bob", "carl", "dell" };
char ranks[4] = { 'A', 'B', 'C', 'D' };
int score[4] = { 5, 6, 7, 8 };
cout << std::boolalpha;
int hoge;
auto student0 = tie(names[0], ranks[0], score[0]);
// => name: bob, rank: B, score: 6
tie(std::ignore, std::ignore, hoge) = student0;
cout << "0> " << std::is_same<int, decltype(hoge)>::value;
cout << ", " << std::is_same<int&, decltype(hoge)>::value << endl;
display_separator();
auto student1 = make_tuple(names[1], ranks[1], score[1]);
// => name: carl, rank: C, score: 7
auto [_1, _2, piyo] = student1;
cout << "1> " << std::is_same<int, decltype(piyo)>::value;
cout << ", " << std::is_same<int&, decltype(piyo)>::value << endl;
display_separator();
auto student2 = tie(names[2], ranks[2], score[2]);
// => name: carl, rank: C, score: 7
auto [_3, _4, fuga] = student2;
cout << "2> " << std::is_same<int, decltype(fuga)>::value;
cout << ", " << std::is_same<int&, decltype(fuga)>::value << endl;
display_separator();
auto student3 = make_tuple(names[2], ranks[2], std::ref(score[2]));
// => name: carl, rank: C, score: 7
auto [_5, _6, pika] = student3;
cout << "3> " << std::is_same<int, decltype(pika)>::value;
cout << ", " << std::is_same<int&, decltype(pika)>::value << endl;
display_separator();
cout << "bonus> "
<< std::is_same<int&, decltype(std::get<2>(student0))>::value << ", "
<< std::is_same<int&, decltype(std::get<2>(student1))>::value << endl;
return 0;
}
// filename: ch1-tuple-example-4.cpp
// compile this> g++ ch1-tuple-example-4.cpp -o ch1-tuple-example-4.exe -std=c++17
这份代码的运行结果如下所示:
0> true, false
--------
1> true, false
--------
2> false, true
--------
3> false, true
--------
bonus> true, true
和之前类似,我们继续来逐行解析这份代码:
- 这个例子我们使用了 C++11 所提供的类型判别机制,这个机制作用在编译期,
std::is_same<U, V>
判断类型 U
和 V
是否为同一类型,decltype()
则是在编译时解析类型,借助这一套工具我们就能很简单的判断目标变量的具体类型是什么了;
- 第 52-54 行测试了
std::get()
的返回类型,注意到不管元组是如何构造的,std::get()
的返回类型永远是引用类型,原因是他的返回值不仅可以作为右值进行计算,也可以作为左值进行赋值并且改变其在 tuple
中的值,那么在设计上自然需要将其的返回类型设置为一个引用类型;
- 第 19-24 行测试了
std::tie()
解包时所解包数据 hoge
的返回类型,由于一开始已经声明 hoge
为 int
,所以自然会在判断是否为 int
时返回 ture
;
- 第 28-32 行测试了通过
std::make_tuple()
创建的元组在 auto [_1, _2, piyo]
解包是的表现,注意到 piyo
和上面的例子中的表现相同,这是由于 std::make_tuple()
并不会创建引用,所以导致了后续操作里不会出现引用类型了;
- 第 36-40 行测试了通过
std::tie()
创建的元组的解包表现,注意到由于 std::tie()
创建的是引用,所以后续解包时产生的变量也是引用;
- 第 44-48 行测试了显式构造引用类型的数据的解包表现,根据前面的分析我们不难理解其结果。
最后我们总结一下关于元组的目前已经解析的信息:
std::make_tuple()
以及 std::tie()
均可以构造一个 tuple
,两者的区别在于前者构造复制,后者创建引用,更准确的说法是后者创建一个左值引用;
std::get()
会解包出来固定位置的值,其返回值是元组内部值的引用;
std::tie()
会解包出来该元组的所有值,不需要的位置可以用占位符 std::ignore
代替,解包数据的数据类型是基本类型而非引用类型;
- C++17 引入的 structured binding 方法
auto [_1, _2, _3, ...]
在解包时的表现与 std::tie()
类似。
与 pair
类似,tuple
提供了一种更为简便的包装数据的方法,和 pair
一样不限制所包装的数据的类型,同时不限制所包装的数据的个数。我们甚至可以把它看做一个特殊的列表。基于这样的认识,我们再来认识这三个工具 std::tuple_cat()
,std::tuple_size()
以及 std::tuple_element
。注意,以下内容均在不同程度上涉及了模板编程的知识,初学者可以先行放弃待积累了一定的模板编程经验之后再重新学习本小节的知识。
以下内容初学者可跳过
首先转变观念,把 tuple
看做一种特殊的要求每一个元素的数据类型的不定长列表。基于这样的考虑,我们先给自己设置一个目标,实现两个元组的拼接操作。拼接其实并不难,因为如果已知两个元组内部的元素之后只需要提取出来然后 std::make_tuple()
就行了。这个问题的难点就在于,由于 C++ 要求所有的变量类型必须在编译时声明,所以如果要实现两个元组的拼接就需要对它们分别进行解包操作,同时还需要声明数据类型。所以现在我们关注这样一个问题,给定一个元组,能否实现以下的三种操作:自动解包,得到元组内部元素的个数,得到确定某一元素的数据类型。这时需要注意,刚刚所提到的三种操作实际上只是一种操作。得到元组内部元素的个数,等价于自动解包时把元组内部的每一个元素都看作自然数 1 然后把解包操作修改为自然数的加法。而得到确定某一元素的数据类型则更为方便,我们只需要在某一个位置上应用一次 decltype
就大功告成了。所以基于以上的思路,我们的任务只有一个,就是如何自动分解元组。
这里一种很简单的方法就是递归处理,每一次把当前元组分解为两部分,当前元祖的头部(第一个元素)和剩余部分,之后再对剩余部分进行处理,直到剩余部分是一个只包含一个元素的元组为止。基于这一思路,我们可以写出下面所示的简单代码。
#include <iostream>
#include <string>
#include <tuple>
template<class Func, class Tuple, int N>
struct TupleHelper
{
static void func(Func f, const Tuple &_)
{
TupleHelper<Func, Tuple, N - 1>::func(f, _);
f(std::get<N - 1>(_), N - 1);
}
};
template<class Func, class Tuple>
struct TupleHelper<Func, Tuple, 1>
{
static void func(Func f, const Tuple &_)
{
f(std::get<0>(_), 0);
}
};
template<class Func, class ...Args>
void manipulate_tuple(Func f, const std::tuple<Args...> &_)
{
TupleHelper<Func, decltype(_), sizeof...(Args)>::func(f, _);
}
template<class ...Args>
void print_tuple(const std::tuple<Args...> &_)
{
std::cout << "(";
manipulate_tuple(
[](auto _, std::size_t idx)
{
if (idx != 0) std::cout << ", ";
std::cout << _;
}, _);
std::cout << ")";
}
int main()
{
print_tuple(std::make_tuple(10, 1.5, 'A'));
// => (10, 1.5, A)
return 0;
}
// filename: ch1-tuple-helper.cpp
// compile this> g++ ch1-tuple-helper.cpp -o ch1-tuple-helper.exe -std=c++14
在这份代码中,我们实现了一个对 tuple
内部的所有元素都进行一次 Func
操作的 TupleHelper
工具。在第 5 行的模板声明中, class Func
被假设为一个接受元组内元素和所在位置 idx
这两个参数的函数类型,int N
则表示这个元组的长度。在这里我们使用的是递归处理的方法,所以在第 15-22 行我们对模板进行了特化处理。另外我们还将 TupleHelper
封装到了函数 manipulate_tuple
当中,利用 decltype
获取元组的具体类型,用变长模板参数获取 tuple
的内部所有元素的个数。一切准备就绪之后就交给编译器处理就行了,因为模板的所有操作都是在编译时完成的。注意这个 TupleHelper
并不是 bug free 的,因为没有处理空元组 tuple<>
的情况,具体怎么修改就仁者见仁智者见智了。
既然了解了如何自动解包,那么我们完全可以独立的借助刚才的组件自行开发一个完成 tuple
合并操作的工具。不过,如果你已经充分理解了借助模板来递归分解元组的这个操作,你完全可以开始使用标准库所提供的三个组件 std::tuple_cat()
,std::tuple_size()
以及 std::tuple_element
来构建自己的程序,因为事到如今你自己就会感慨,这三者的实现并不见得会有多么高深莫测。
让我们回到上一份例子中,为了简便起见我们把上一份代码的第 5-41 行封装进 tuple_utils.hpp
当中,同时包装到命名空间 __utils
以防止与之后的内容混淆。封装后的代码如下:
// filename: tuple_utils.hpp
#ifndef __TUPLE_UTILS_HPP__
#define __TUPLE_UTILS_HPP__
namespace __utils{
/// same as ch1-tuple-helper.cpp
template<class Func>
void manipulate_tuple(Func f, const std::tuple<> &_)
{
std::cerr << "Warning! Manipulating an EMPTY tuple.";
};
} // namespace __utils
#endif // __TUPLE_UTILS_HPP__
在拥有了一个封装好的工具盒之后,我们来看看标准库所提供的剩下的三个工具的作用。
#include <iostream>
#include <tuple>
#include <utility>
#include "tuple_utils.hpp"
int main()
{
auto t1 = std::make_tuple(10, "abc", 'B', 32);
int n = 11;
auto t2 = std::make_tuple("bcd", 'C', 64);
auto t3 = std::tuple_cat(t1, std::tie(n), t2, std::make_pair("abc", "def"));
n = 16;
__utils::print_tuple(t3); std::cout << std::endl;
// => (10, abc, B, 32, 16, bcd, C, 64, abc, def)
std::cout << std::tuple_size<decltype(t3)>::value << std::endl;
// => 10
std::tuple_element<1, decltype(t3)>::type hoge = "test";
std::cout << hoge << std::endl; // => "test"
std::tuple<> t_empty;
__utils::print_tuple(t_empty);
// => (Warning! Manipulating an EMPTY tuple.)
return 0;
}
// filename: ch1-tuple-tools.cpp
// compile this> g++ ch1-tuple-tools.cpp -o ch1-tuple-tools.exe -std=c++14
第 12 行测试了 std::tuple_cat()
方法,该方法会合并多个 tuple
同时保持他们的引用关系。那么既然要保持内部的引用关系,想必在解包时传递的参数应当是一个左值,这样就保证了后续的引用操作依旧有效。第 17 行和第 19 行测试了 std::tuple_size()
和 std::tuple_element()
方法,分别获取元组内部数据的个数以及指定位置数据类型。注意到这两个方法都是在编译时生效的,所以相比较于 python
的列表还是逊色了不少。代码的第 23 行测试了我们的 __utils::print_tuple()
面对空 tuple
的处理方法,对应的代码在 tuple_utils.hpp
的第 26-30 行。
既然已经把 tuple
看做一种列表了,那我们索性把超纲进行到底。初学 Haskell 的时候大家都遇见过关于列表的四个函数,函数 head
返回列表的头部,也就是第一个元素,tail
返回列表除去头部的剩下部分,last
返回列表的最后一个元素,init
返回列表除去最后一个元素的剩余部分。
head [5, 4, 3, 2, 1] -- => 5
tail [5, 4, 3, 2, 1] -- => [4, 3, 2, 1]
init [5, 4, 3, 2, 1] -- => [5, 4, 3, 2]
last [5, 4, 3, 2, 1] -- => 1
那么如何用 C++ 来实现这四种功能呢?第一种方法就是我们之前提到的递归分解,把大化小,最后在组装回去。不过,如果对变长模板更熟悉一些,我们可以直接在编译期做更多的事情。我们先来看一下 head
与 tail
这两个函数的实现:
#include <iostream>
#include <tuple>
#include <utility>
#include "tuple_utils.hpp"
template<typename T, typename ...Ts>
auto head(std::tuple<T, Ts...> t)
{
return std::get<0>(t);
}
template<std::size_t ...Ns, typename ...Ts>
auto tail_impl(std::index_sequence<Ns...>, std::tuple<Ts...> t)
{
return std::make_tuple(std::get<Ns + 1u>(t)...);
}
template <typename ...Ts>
auto tail(std::tuple<Ts...> t)
{
return tail_impl(std::make_index_sequence<sizeof...(Ts) - 1u>() , t);
}
int main()
{
auto t = std::make_tuple(2, 3.14, 'c');
std::cout << head(t) << std::endl; // => 2
__utils::print_tuple(tail(t)); std::cout << std::endl; // => (3.14, 'c')
return 0;
}
// filename: ch1-tuple-head-and-tail.cpp
// compile this> g++ ch1-tuple-head-and-tail.cpp -o ch1-tuple-head-and-tail.exe -std=c++14
这份代码中我们又借助了一个强大的 C++14 的编译时工具 std::index_sequence
,它表示一串只在编译期生效的整数数列。不过一般而言更常用的是 std::make_index_sequence
,他会在编译期生成一串 0, 1, 2, ...
这样的数列,这样就更加方便了我们对 tuple
的读写操作。比如我们现在需要实现 tail
函数的操作,除了第一项以外把剩下的 N-1 个数据返回成 tuple
,我们就可以先用 std::make_index_sequence
制造一个长度为 N-1 的 0, 1, 2, ...
数列,然后对每一项 +1 就正好对应上了我们所要的剩下的元素的位置,最后用 std::get()
获取元素,顺便在外层包装上 std::make_tuple()
即可。既然已经看到了 std::make_index_sequence
这个工具,那么就不妨来试一下实现 init
和 last
函数吧。
#include <iostream>
#include <tuple>
#include <utility>
#include "tuple_utils.hpp"
template<std::size_t ...Ns, typename ...Ts>
auto init_impl(std::index_sequence<Ns...>, std::tuple<Ts...> t)
{
return std::make_tuple(std::get<Ns>(t)...);
}
template<typename ...Ts>
auto init(std::tuple<Ts...> t)
{
return init_impl(std::make_index_sequence<sizeof...(Ts) - 1u>() , t);
}
template<typename ...Ts>
auto last(std::tuple<Ts...> t)
{
return std::get<sizeof...(Ts) - 1u>(t);
}
template <class T>
auto size(T t)
{
return std::tuple_size<T>::value;
}
template <class T>
auto last2(T t)
{
return std::get<std::tuple_size<T>::value - 1u>(t);
}
int main()
{
auto t = std::make_tuple(2, 3.14, 'c');
std::cout << size(t) << std::endl; // => 3
std::cout << last(t) << std::endl; // => 'c'
std::cout << last2(t) << std::endl; // => 'c'
__utils::print_tuple(init(t)); std::cout << std::endl; // => (2, 3.14)
return 0;
}
// filename: ch1-tuple-init-and-last.cpp
// compile this> g++ ch1-tuple-init-and-last.cpp -o ch1-tuple-init-and-last.exe -std=c++14
最后给一个思考题,如果我把上一份代码中 last2()
的实现写做
template <class T> auto last2(T t)
{
return std::get<size<T>(t) - 1u>(t);
}
这份代码能运行吗?为什么?
A tuple
is a C++11 construction and it is built heavily on variadic templates.
A tuple
is a variadic class template that stores an unlimited set of values of different types, defined when instantiating the tuple; for example:
tuple<int, int> x;
will store 2 integers.
tuple<string, int, bool> y;
will store one string, one integer and one boolean and so on.
Tuples are useful for several things:
- They are light replacements for structures.
- They can be useful to return several values from a function
- They let you perform comparisons through all the value set
You can instantiate tuples in these ways:
tuple<int, int, bool> x; //instantiating but not initializing
tuple<int, string> y { 2, "hello" }; //instantiating AND initializing
auto z = make_tuple(2, 3, 4, "bye"s); //instantiating AND initializing throught the make_tuple helper function.
Consider you declare something like this:
auto xx = make_tuple(3.14, "PI"s);
To get the tuple values, you must to use the get<int>()
function like in this code excerpt:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
#include <tuple>
#include <iostream>
#include <string>
using namespace std;
int main()
{
auto xx = make_tuple(3.14, "PI" s);
auto & first = get<0>(xx);
auto & second = get<1>(xx);
cout << "(" << first << "; " << second << ")" << endl;
return 0;
}
|
The template parameter is the index of the element to be retrieved, so, if the first element was an integer, the index 0 will retrieve the integer stored as the first value in the tuple, the index 0 will retrieve the second one and so on.
Tuples are also very useful to create structs “on the fly” and to use them in data structures; for example:
1
2
3
4
5
6
7
8
9
10
11
12
|
int main()
{
vector<tuple< int , string>> vec;
vec.push_back(make_tuple(10, "ten" ));
vec.emplace_back(20, "twenty" );
vec.emplace_back(30, "thirty" );
for ( auto & i : vec)
{
cout << "(" << get<0>(i) << "; " << get<1>(i) << ")" << endl;
}
}
|
In this case I did not need to create a new struct in order to store the number and the number name, for example.
Other nice thing on tuples is that you can compare tuples relying on the comparison operators of the data types inside the tuples.
Look at this code:
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 <set>
#include <string>
#include <tuple>
using namespace std;
using car = tuple<string, string, int >;
void print( const car& c)
{
cout << get<0>(c) << ", " << get<1>(c) << "; " << get<2>(c) << endl;
}
int main()
{
set<car> cars;
cars.emplace( "Toyota" , "Rav4" , 2012);
cars.emplace( "VW" , "Jetta" , 2015);
cars.emplace( "Chevrolet" , "Sonic" , 2013);
cars.emplace( "BMW" , "X5" , 2014);
cars.emplace( "VW" , "Jetta" , 2014);
for ( auto & i : cars)
print(i);
cout << "******" << endl;
auto it = cars.find(car { "Toyota" , "Rav4" , 2012 });
if (it == cars.end())
cerr << "CAR NOT FOUND" << endl;
else
print(*it);
return 0;
}
|
When it is executed, this thing is displayed:
BMW, X5; 2014
Chevrolet, Sonic; 2013
Toyota, Rav4; 2012
VW, Jetta; 2014
VW, Jetta; 2015
******
Toyota, Rav4; 2012
The code and its execution contain several interesting things:
- I defined an alias to a tuple called “car”, having it, I was able to use the tuple as a declared type in a lot of cases… without declaring it!
- If I would have created a “car” struct, to use it inside a set, I would have to overload the operator<() for the struct, because the sets use this operator in order to insert the objects accordingly.
- When executing the program, all the objects where ordered by brand, by model and by year; using the tuple operator<(). If the first element in a tuple A is less than the first element in a tuple B, the operator returns TRUE. If the first element in tuple A is greater than the first element in tuple B, the operator returns FALSE; but if both elements are equal, the comparison is performed between the second elements and so on.
- Notice I created a car to pass it into the method find; I created it in exactly the same way I would have created a car struct instance.
- Notice the element is found in the set (because operator<() too).
Now I want to modify my set in order to store the elements ordered by year, by brand and by model. Look at the new implementation:
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
|
#include <iostream>
#include <set>
#include <string>
#include <tuple>
using namespace std;
using car = tuple<string, string, int >;
void print( const car& c)
{
cout << get<0>(c) << ", " << get<1>(c) << "; " << get<2>(c) << endl;
}
int main()
{
auto cc = []( auto & a, auto & b)
{
return tie(get<2>(a), get<0>(a), get<1>(a)) < tie(get<2>(b), get<0>(b), get<1>(b));
};
set<car, decltype (cc)> cars { cc };
cars.emplace( "Toyota" , "Rav4" , 2012);
cars.emplace( "VW" , "Jetta" , 2015);
cars.emplace( "Chevrolet" , "Sonic" , 2013);
cars.emplace( "BMW" , "X5" , 2014);
cars.emplace( "VW" , "Jetta" , 2014);
for ( auto & i : cars)
print(i);
cout << "******" << endl;
auto it = cars.find(car { "Toyota" , "Rav4" , 2012 });
if (it == cars.end())
cerr << "CAR NOT FOUND" << endl;
else
print(*it);
return 0;
}
|
The output is:
Toyota, Rav4; 2012
Chevrolet, Sonic; 2013
BMW, X5; 2014
VW, Jetta; 2014
VW, Jetta; 2015
******
Toyota, Rav4; 2012
The elements are ordered first by year.
Notice my “cc” lambda expression. It uses a function called “tie”.
“tie” is a function that takes a set of references and creates a tuple containing references (not values or copies) to the original values. So, tie is useful to create temporary light tuples. In my example, using “tie”, I was able to create other set of tuples with different logical order, so the tuple operator<() algorithm worked using this new logical order.
1,元组简介
tuple是一个固定大小的不同类型值的集合,是泛化的std::pair。我们也可以把他当做一个通用的结构体来用,不需要创建结构体又获取结构体的特征,在某些情况下可以取代结构体使程序更简洁,直观。std::tuple理论上可以有无数个任意类型的成员变量,而std::pair只能是2个成员,因此在需要保存3个及以上的数据时就需要使用tuple元组了。
tuple(元组)在c++11中开始引用的。tuple看似简单,其实它是简约而不简单,可以说它是c++11中一个既简单又复杂的东东,关于它简单的一面是它很容易使用,复杂的一面是它内部隐藏了太多细节,要揭开它神秘的面纱时又比较困难。
2,tuple的创建和初始化
std::tuple<T1, T2, TN> t1; //创建一个空的tuple对象(使用默认构造),它对应的元素分别是T1和T2...Tn类型,采用值初始化。
std::tuple<T1, T2, TN> t2(v1, v2, ... TN); //创建一个tuple对象,它的两个元素分别是T1和T2 ...Tn类型; 要获取元素的值需要通过tuple的成员get<Ith>(obj)进行获取(Ith是指获取在tuple中的第几个元素,请看后面具体实例)。
std::tuple<T1&> t3(ref&); // tuple的元素类型可以是一个引用
std::make_tuple(v1, v2); // 像pair一样也可以通过make_tuple进行创建一个tuple对象
tuple的元素类型为引用:
std::string name;
std::tuple<string &, int> tpRef(name, 30);
// 对tpRef第一个元素赋值,同时name也被赋值 - 引用
std::get<0>(tpRef) = "Sven";
// name输出也是Sven
std::cout << "name: " << name << '\n';
3,有关tuple元素的操作
等价结构体
开篇讲过在某些时候tuple可以等同于结构体一样使用,这样既方便又快捷。如:
struct person {
char *m_name;
char *m_addr;
int *m_ages;
};
//可以用tuple来表示这样的一个结构类型,作用是一样的。
std::tuple<const char *, const char *, int>
2. 如何获取tuple元素个数
当有一个tuple对象但不知道有多少元素可以通过如下查询:
// tuple_size
#include <iostream> // std::cout
#include <tuple> // std::tuple, std::tuple_size
int main ()
{
std::tuple<int, char, double> mytuple (10, 'a', 3.14);
std::cout << "mytuple has ";
std::cout << std::tuple_size<decltype(mytuple)>::value;
std::cout << " elements." << '\n';
return 0;
}
//输出结果:
mytuple has 3 elements
3.如何获取元素的值
获取tuple对象元素的值可以通过get<Ith>(obj)方法进行获取;
Ith - 是想获取的元素在tuple对象中的位置。
obj - 是想获取tuple的对象
// tuple_size
#include <iostream> // std::cout
#include <tuple> // std::tuple, std::tuple_size
int main ()
{
std::tuple<int, char, double> mytuple (10, 'a', 3.14);
std::cout << "mytuple has ";
std::cout << std::tuple_size<decltype(mytuple)>::value;
std::cout << " elements." << '\n';
//获取元素
std::cout << "the elements is: ";
std::cout << std::get<0>(mytuple) << " ";
std::cout << std::get<1>(mytuple) << " ";
std::cout << std::get<2>(mytuple) << " ";
std::cout << '\n';
return 0;
}
//输出结果:
mytuple has 3 elements.
the elements is: 10 a 3.14
tuple不支持迭代,只能通过元素索引(或tie解包)进行获取元素的值。但是给定的索引必须是在编译器就已经给定,不能在运行期进行动态传递,否则将发生编译错误:
for(int i=0; i<3; i++)
std::cout << std::get<i>(mytuple) << " "; //将引发编译错误
4.获取元素的类型
要想得到元素类型可以通过tuple_element方法获取,如有以下元组对象:
std::tuple<std::string, int> tp("Sven", 20);
// 得到第二个元素类型
std::tuple_element<1, decltype(tp)>::type ages; // ages就为int类型
ages = std::get<1>(tp);
std::cout << "ages: " << ages << '\n';
//输出结果:
ages: 20
5.利用tie进行解包元素的值
如同pair一样也是可以通过tie进行解包tuple的各个元素的值。如下tuple对象有4个元素,通过tie解包将会把这4个元素的值分别赋值给tie提供的4个变量中。
#include <iostream>
#include <tuple>
#include <utility>
int main(int argc, char **argv) {
std::tuple<std::string, int, std::string, int> tp;
tp = std::make_tuple("Sven", 25, "Shanghai", 21);
// 定义接收变量
std::string name;
std::string addr;
int ages;
int areaCode;
std::tie(name, ages, addr, areaCode) = tp;
std::cout << "Output: " << '\n';
std::cout << "name: " << name <<", ";
std::cout << "addr: " << addr << ", ";
std::cout << "ages: " << ages << ", ";
std::cout << "areaCode: " << areaCode << '\n';
return 0;
}
//输出结果:
Output:
name: Sven, addr: Shanghai, ages: 25, areaCode: 21
但有时候tuple包含的多个元素时只需要其中的一个或两个元素,如此可以通过std::ignore进行变量占位,这样将会忽略提取对应的元素。可以修改上述例程:
#include <iostream>
#include <tuple>
#include <utility>
int main(int argc, char **argv) {
std::tuple<std::string, int, std::string, int> tp;
tp = std::make_tuple("Sven", 25, "Shanghai", 21);
// 定义接收变量
std::string name;
std::string addr;
int ages;
int areaCode = 110;
std::tie(name, ages, std::ignore, std::ignore) = tp;
std::cout << "Output: " << '\n';
std::cout << "name: " << name <<", ";
std::cout << "addr: " << addr << ", ";
std::cout << "ages: " << ages << ", ";
std::cout << "areaCode: " << areaCode << '\n';
return 0;
}
//输出结果:
Output:
name: Sven, addr: , ages: 25, areaCode: 110
6. tuple元素的引用
前面已经列举了将引用作为tuple的元素类型。下面通过引用搭配make_tuple()可以提取tuple的元素值,将某些变量值设给它们,并通过改变这些变量来改变tuple元素的值:
#include <iostream>
#include <tuple>
#include <functional>
int main(int argc, char **agrv) {
std::tuple<std::string, int, float> tp1("Sven Cheng", 77, 66.1);
std::string name;
int weight;
float f;
auto tp2 = std::make_tuple(std::ref(name), std::ref(weight), std::ref(f)) = tp1;
std::cout << "Before change: " << '\n';
std::cout << "name: " << name << ", ";
std::cout << "weight: " << weight << ", ";
std::cout << "f: " << f << '\n';
name = "Sven";
weight = 80;
f = 3.14;
std::cout << "After change: " << '\n';
std::cout << "element 1st: " << std::get<0>(tp2) << ", ";
std::cout << "element 2nd: " << std::get<1>(tp2) << ", ";
std::cout << "element 3rd: " << std::get<2>(tp2) << '\n';
return 0;
}
//输出结果:
Before change:
name: Sven Cheng, weight: 77, f: 66.1
After change:
element 1st: Sven, element 2nd: 80, element 3rd: 3.14
————————————————
版权声明:本文为CSDN博主「sevencheng798」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/sevenjoin/article/details/88420885