目录
1 类型推导
1.1 auto类型推导
auto x= 5; //OK:x是int类型
auto pi = new auto(1); //OK: pi被推导为int*
const auto *V = &x, u=6; //OK:v是const int*类型,u是const int类型
static auto y = 0.0; //OK:y是double类型
auto int r; //error: auto不再表示存储类型指示符
auto s; //error: auto无法推导出s 的类型
看一个例子,在一个unordered_multimap中查找一个范围,代码如下:
#include <map>
int main (void)
{
std: :unordered_multimap<int, int>resultMap;
//...
std::pair<std::unordered_multimap<int,int>::iterator, std:: unordered_multimap<int, int>::iterator>
range = resultMap.equal_range(key);
return 0;
)
这个equal_range返回的类型声明显得烦琐而冗长,而且实际上并不关心这里的具体类型(大概知道是一个std::pair就够了)。这时,通过auto就能极大的简化书写,省去推导具体类型的过程:
#include <map>
int main (void)
(
std::unordered_multimap<int, int>map;
//...
auto range = map.equal_range(key);
return 0;
}
1.2 decltype类型推导
int x = 0;
decltype(x) y = 1; //y -> int
decltype (x + y) z = 0; //z -> int
const int& i = x ;
decltype(i) j = y; //j-> const int &
const decltype(z) * p = &z; //*p -> const int, p -> const int *
decltype (z) * pi = &z; //*pi -> int, pi -> int *
decltype(pi)* pp = π //*pp -> int *, pp -> int **
2 模板的改进
2.1 模板的别名
//重定义unsigned int
typedef unsigned int uint_t;
using uint_t = unsigned int;
//重定义std::map
typedef std::map<std::string, int> map_int_t;
using map_int_t = std::map<std::string, int>;
2.2 函数模板的默认参数
在C++98/03中,类模板可以有默认的模板参数,如下:
template <typename T, typename U = int, U N = 0>
struct Foo
{
//...
};
但是却不支持函数的默认模板参数:
template <typename T = int> //error in C++98/03: default template arguments
void func (void)
{
//...
}
现在这一限制在C++11中被解除了,上面的func函数在C++11中可以直接使用.
3 列表初始化
我们知道,在C++98/03中的对象初始化方法有很多种:
int arr[3] = { 1, 2, 3 }; //普通数组
struct A
{
int x;
struct B
{
int i;
int j;
} b;
} a = { 1, {2, 3} }; //POD类型
//拷贝初始化
int i = 0;
class Foo
{
public:
Foo(int){}
} foo = 123;
//直接初始化
int j(0);
Foo bar(123);
这些不同的初始化方法,都有各自的适用范围和作用。最关键的是,这些种类繁多的初始化方法,没有一种可以通用所有情况。
为了统一初始化方式,并且让初始化行为具有确定的效果,C++11中提出了列表初始化(List-initialization)的概念。
在C++11中,可以直接在变量名后面跟上初始化列表,来进行对象的初始化。
这种变量名后面跟上初始化列表方法同样适用于普通数组和POD类型的初始化:
int arr[3] { 1, 2, 3 } ; //普通数组
struct A
{
int x;
struct B
{
int i;
int j;
} b;
} a { 1, { 2, 3 } }; //POD类型
在初始化时,{}前面的等于号是否书写对初始化行为没有影响。
另外,如同读者所想的那样,new操作符等可以用圆括号进行初始化的地方,也可以使用初始化列表:
int* a = new int { 123 };
double b = double { 12.12 };
int* arr = new int [3]{ 1, 2, 3 };
指针a指向了一个new操作符返回的内存,通过初始化列表方式在内存初始化时指定了值为123。
b则是对匿名对象使用列表初始化后,再进行拷贝初始化。
这里让人眼前一亮的是arr的初始化方式。堆上动态分配的数组终于也可以使用初始化列表进行初始化了。
除了上面所述的内容之外,列表初始化还可以直接使用在函数的返回值上:
struct Foo
{
Foo (int,. double){ }
};
Foo func(void)
{
return { 123, 321.0 };
}
这里的return语句就如同返回了一个Foo(123, 321.0)。
由上面的这些例子可以看到,在C++11中使用初始化列表是非常便利的。它不仅统一了各种对象的初始化方式,而且还使代码的书写更加简单清晰。
下面重点介绍当类型是一个类时的情况。首先是存在用户自定义构造函数时的例子,代码如下:
struct Foo
{
int x;
double y;
int z;
Foo (int, int) { }
};
Foo foo { 1, 2.5, 1 }; //error
这时无法将Foo看做一个聚合类型,因此,必须以自定义的构造函数来构造对象。私有(Private)或保护(Protected)的非静态数据成员的情况如下:
struct ST
{
int x;
double y;
protected:
int z;
};
ST s { 1, 2.5, 1 }; //error
struct Foo
{
int x;
double y;
protected:
static int z;
};
Foo foo { 1, 2.5 }; //ok
在上面的示例中,ST的初始化是非法的。因为ST的成员z是一个受保护的非静态数据成员。
而Foo的初始化则是成功的,因为它的受保护成员是一个静态数据成员。
这里需要注意,Foo中的静态成员是不能通过实例foo的初始化列表进行初始化的,它的初始化遵循静态成员的初始化方式。
对于有基类和虚函数的情况:
在上面的示例中,ST的初始化是非法的。因为ST的成员z是一个受保护的非静态数据成员。
而Foo的初始化则是成功的,因为它的受保护成员是一个静态数据成员。
这里需要注意,Foo中的静态成员是不能通过实例foo的初始化列表进行初始化的,它的初始化遵循静态成员的初始化方式。
对于有基类和虚函数的情况:
struct ST
{
int x;
double y;
virtual void F(){}
};
ST s { 1, 2.5 }; //error
struct Base {};
struct Foo : public Base
{
int x;
double y;
};
Foo foo { 1, 2.5 }; //error
ST和Foo的初始化都会编译失败。因为ST中存在一个虚函数F,而Foo则有一个基类Base。
最后,介绍“不能有{}和=直接初始化 (brace-or-equal-initializer)的非静态数据成员”这条规则,代码如下:
struct ST
{
int x;
double y = 0.0 ;
};
ST s { 1,2.5 }; //error
在ST中,y在声明时即被=直接初始化为0.0,因此,ST并不是一个聚合类型,不能直接使用初始化列表。
对于上述非聚合类型的情形,想要使用初始化列表的方法就是自定义一个构造函数,比如:
struct ST
{
int x;
double y;
virtual void F() { }
private:
int z;
public:
ST(int i, double j, int k) : x(i), y(j), z(k){ }
} ;
ST s { 1, 2.5, 2 };
需要注意的是,聚合类型的定义并非递归的。简单来说,当一个类的非静态成员是非聚合类型时,这个类也有可能是聚合类型。比如下面这个例子:
struct ST
{
int x;
double y;
private:
int z;
};
ST s { 1, 2.5, 1 }; //error
struct Foo
{
ST st;
int x;
double y;
};
Foo foo { {}, 1, 2.5 }; //oK
可以看到,ST并非一个聚合类型,因为它有一个 Private的非静态成员。
但是尽管Foo含有这个非聚合类型的非静态成员st,它仍然是一个聚合类型,可以直接使用初始化列表。
任意长度的初始化列表:
class Foo
{
public:
Foo (std::initializer_list<int>) {}
};
Foo foo = {1, 2, 3, 4, 5 }; //oK!
std::initializer_list不仅可以用来对自定义类型做初始化,还可以用来传递同类型的数据集合,代码如下:
void func(std::initializer_list<int> l)
{
for (auto it = l.begin(); it !=l.end (); ++it)
{
std::cout << *it << std::endl;
}
}
int main (void)
{
func({ }); //一个空集合
func ({ 1, 2, 3 }); //传递{ 1, 2, 3 }
return 0;
}
了解了std::initializer_list之后,再来看看它的一些特点,如下:
- 它是一个轻量级的容器类型,内部定义了iterator等容器必需的概念。
- 对于std:.initializer_list而言,它可以接收任意长度的初始化列表,但要求元素必须是同种类型T(或可转换为T)。
- 它有3个成员接口: size()、begin()、end()。它只能被整体初始化或赋值。
通过前面的例子,已经知道了std:;initializer_list的前几个特点。其中没有涉及的接口size()是用来获得std::initializer_list的长度的,比如:
std::initializer_list<int> list = { 1, 2, 3 };
size_t n = list.size() ; //n = 3
最后,对std::initializer_list的访问只能通过begin()和 end()进行循环遍历,遍历时取得的迭代器是只读的。因此,无法修改std:;initializer_list中某一个元素的值,但是可以通过初始化列表的赋值对std::initializer_list做整体修改,代码如下:
std::initializer_list<int> list;
size_t n = list.size() ; //n = 0
list = { 1, 2, 3, 4, 5 };
n = list.size(); //n = 5
list = {3, 1, 2, 4 };
n = list.size() ; //n = 4
std::initializer_list拥有一个无参数的构造函数,因此,它可以直接定义实例,此时将得到一个空的std::.initializer_list。
之后,我们对std::initializer_list进行赋值操作(注意,它只能通过初始化列表赋值),可以发现std::initializer_list被改写成了{1, 2, 3, 4, 5}。
然后,还可以对它再次赋值,std::initializer_list被修改成了{3, 1, 2, 4}。
std:.initializer_list的内部并不负责保存初始化列表中元素的拷贝,仅仅存储了列表中元素的引用而已。
因此,我们不应该像这样使用:
std::initializer_list<int> func (void)
{
int a = 1, b = 2 ;
return { a, b }; //a、b在返回时并没有被拷贝
}
虽然这能够正常通过编译,但却无法传递出我们希望的结果(a、b在函数结束时,生存期也结束了,因此,返回的将是不确定的内容)。
这种情况下最好的做法应该是这样:
std::vector<int> func (void)
{
int a = 1, b = 2;
return { a, b };
}
使用真正的容器,或具有转移/拷贝语义的物件来替代std::initializer_list返回需要的结果。
列表初始化防止类型收窄:
int a = 1.1; //OK
int b = { 1.1 }; //error
float fa = 1e40; //OK
float fb = { 1e40 } ; //error
float fc = (unsigned long long)-1; //OK
float fd = { (unsigned long long)-1 }; //error
float fe = (unsigned long long)1; //OK
float ff = { (unsigned long long)1 }; //OK
const int x = 1024, y = 1;
char c = x ; //OK
char d = { x } //error
char e = y; //OK
char f = { y }; //OK
4 基于范围的for循环
#include <iostream>
#include <vector>
int main (void)
{
std::vector<int> arr = { 1,2,3 };
//...
for(auto n : arr) //使用基于范围的for循环
std: :cout<<n << std::endl;
return 0;
}
接下来,看看基于范围的 for循环对容器的访问频率。看下面这段代码:
#include <iostream>
#include <vector>
std::vector<int> arr = { 1, 2, 3, 4, 5 };
std::vector<int>& get_range (void)
{
std: :cout << "get_range ->: "<< std::endl;
return arr;
}
int main (void)
{
for (auto val : get_range ())
std::cout << val << std::endl;
return 0;
}
输出结果:
get_range ->:
1
2
3
4
5
从上面的结果中可以看到,不论基于范围的 for循环迭代了多少次,get_range()只在第一次迭代之前被调用。
因此,对于基于范围的 for循环而言,冒号后面的表达式只会被执行一次。
最后,让我们看看在基于范围的for循环迭代时修改容器会出现什么情况。比如,下面这段代码:
#include <iostream>
#include <vector>
int main (void)
{
std::vector<int> arr = { 1, 2, 3, 4, 5 };
for (auto val : arr)
{
std::cout << val << std::endl;
arr.push_back(0);扩大容器
}
return 0;
}
执行结果(32位 mingw4.8 ):
1
5189584
-17891602
-17891602
-17891602
若把上面的vector换成list,结果又将发生变化。
这是因为基于范围的for循环其实是普通for循环的语法糖,因此,同普通的for循环-样,在迭代时修改容器很可能会引起迭代器失效,导致一些意料之外的结果。由于在这里我们是看不到迭代器的,因此,直接分析对基于范围的for循环中的容器修改会造成什么样的影响是比较困难的。
5 std::function和 bind绑定器
5.1 std::function可调用对象包装器
std:function是可调用对象的包装器。它是一个类模板,可以容纳除了类成员(函数)指针之外的所有可调用对象。通过指定它的模板参数,它可以用统一的方式处理函数、函数对象、函数指针,并允许保存和延迟执行它们。
#include <iostream>
#include <functional>
void func(void)
{
std::cout << __FUNCTION__ << std::endl;
}
class Foo
{
public:
static int foo_func(int a)
{
std::cout << __FUNCTION__ << "(" << a << ") ->: ";
return a;
}
};
class Bar
{
public:
int operator()(int a)
{
std::cout << __FUNCTION__ << "(" << a << ") ->: ";
return a;
}
};
int main(void)
{
//绑定普通函数
std::function<void(void)> fr1 = func;
fr1();
//绑定一个类的静态成员函数
std::function<int(int)> fr2 = Foo::foo_func;
std::cout << fr2(123) << std::endl;
//绑定一个仿函数
Bar bar;
fr2 = bar;
std::cout << fr2(123) << std::endl;
system("pause");
return 0;
}
运行结果如下:
func
foo_func (123)->: 123
operator () (123)->: 123
从上面我们可以看到std::function 的使用方法,当我们给std:function填入合适的函数签名(即一个函数类型,只需要包括返回值和参数表)之后,它就变成了一个可以容纳所有这一类调用方式的“函数包装器”。
std::function还可以作为函数入参:
#include <iostream>
#include <functional>
void call_when_even(int x, const std::function<void(int)>& f)
{
if (!(x & 1)) //x % 2 == 0
{
f(x);
}
}
void output(int x)
{
std::cout << x << " ";
}
int main(void)
{
for (int i = 0; i < 10; ++i)
{
call_when_even(i, output);
}
std::cout << std::endl;
system("pause");
return 0;
}
输出结果如下:
0 2 4 6 8
5.2 std::bind绑定器
std::bind用来将可调用对象与其参数一起进行绑定。绑定后的结果可以使用std::function进行保存,并延迟调用到任何我们需要的时候。
通俗来讲,它主要有两大作用:
1)将可调用对象与其参数一起绑定成一个仿函数。
2)将多元(参数个数为n,n>1)可调用对象转成一元或者(n-1)元可调用对象,即只绑定部分参数。
#include <iostream>
#include <functional>
void call_when_even(int x, const std::function<void(int)>& f)
{
if (!(x & 1)) //x % 2 == 0
{
f(x);
}
}
void output(int x)
{
std::cout << x << " ";
}
void output_add_2(int x)
{
std::cout << x + 2 << " ";
}
int main(void)
{
{
auto fr = std::bind(output, std::placeholders::_1);
for (int i = 0; i < 10; ++i)
{
call_when_even(i, fr);
}
std::cout << std::endl;
}
{
auto fr = std::bind(output_add_2, std::placeholders::_1);
for (int i = 0; i < 10; ++i)
{
call_when_even(i, fr);
}
std::cout << std::endl;
}
system("pause");
return 0;
}
输出结果如下:
0 2 4 6 8
2 4 6 8 10
同样还是上面std::function中最后的一个例子,只是在这里,我们使用了std::bind,在函数外部通过绑定不同的函数,控制了最后的执行结果。
我们使用auto fr保存std::bind的返回结果,是因为我们并不关心std::bind真正的返回类型(实际上 std::bind的返回类型是一个stl内部定义的仿函数类型),只需要知道它是一个仿函数,可以直接赋值给一个std::function。当然,这里直接使用std::function类型来保存std::bind的返回值也是可以的。
std::placeholders::_1是一个占位符,代表这个位置将在函数调用时,被传人的第一个参数所替代。
因为有了占位符的概念,std:bind的使用非常灵活:
#include <iostream>
#include <functional>
void output(int x, int y)
{
std::cout << x << " " << y << std::endl;
}
int main(void)
{
std::bind(output, 1, 2)();
std::bind(output, std::placeholders::_1, 2)(1);
std::bind(output, 2, std::placeholders::_1)(1);
std::bind(output, 2, std::placeholders::_2)(1); //error:调用时没有第二个参数
std::bind(output, 2, std::placeholders::_2)(1, 2); //输出 2 2 调用时第一个参数被吞掉了
std::bind(output, std::placeholders::_1, std::placeholders::_2)(1, 2); //输出 1 2
std::bind(output, std::placeholders::_2, std::placeholders::_1)(1, 2); //输出 2 1
system("pause");
return 0;
}
上面对std::bind的返回结果直接施以调用。可以看到,std::bind可以直接绑定函数的所有参数,也可以仅绑定部分参数。
在绑定部分参数的时候,通过使用std::placeholders,来决定空位参数将会属于调用发生时的第几个参数。
std::bind和std::function配合使用
#include <iostream>
#include <functional>
class A
{
public:
int i_ = 0;
void output(int x, int y)
{
std::cout << x << " " << y << std::endl;
}
};
int main(void)
{
A a;
std::function<void(int, int)> fr = std::bind(&A::output, &a, std::placeholders::_1, std::placeholders::_2);
fr(1, 2); //输出 1 2
std::function<int&(void)> fr_i = std::bind(&A::i_, &a); //vs13的bug,绑定成员变量要报错
fr_i() = 123;
std::cout << a.i_ << std::endl; //输出 123
system("pause");
return 0;
}
fr的类型是std::function<void(int,int)>。我们通过使用std::bind,将A的成员函数output的指针和a绑定,并转换为一个仿函数放入fr中存储。之后, std::bind将A的成员i_的指针和a绑定,返回的结果被放入 std::function<int&(void)>中存储,并可以在需要时修改访问这个成员。
其实bind简化和增强了之前标准库中bind1st和 bind2nd,它完全可以替代bind1s和bind2st,并且能组合函数。我们知道,bind1st和 bind2nd 的作用是将一个二元算子转换成一个一元算子,代码如下:
//查找元素值大于10的元素个数
int count = std::count_if(coll.begin(), coll.end(), std::bindlst(less<int>(), 10));
//查找元素之小于10的元素个数
int count = std::count_if(coll.begin(), coll.end(), std::bind2nd(less<int>(), 10)) ;
本质上是对一个二元函数less的调用,但是它却要分别用bind1st和bind2nd,并且还要想想到底是用bind1st 还是bind2nd,用起来十分不便。
现在我们有了bind,就可以以统一的方式去实现了,代码如下:
using std::placeholders::_1;
//查找元素值大于10的元素个数
int count = std::count_if(col1.begin(), coll.end() ,std::bind(less<int>(), 10, _1));
//查找元素之小于10的元素个数
int count = std::count_if(coll.begin(), coll.end(), std::bind(less<int>(), _1, 10)) ;
这样就不用关心到底是用bind1st还是bind2nd,只需要使用bind 即可。
bind还有一个强大之处就是可以组合多个函数:
using std::placeholders::_1;
//查找集合中大于5小于10的元素个数
auto f = std::bind(std::logical_and<bool>(), std::bind(std::greater<int>(), _1, 5), std::bind(std::less_equal<int>(), _1, 10));
int count = std::count_if(coll.begin(), coll.end(), f);
6 lambda表达式
lambda表达式定义了一个匿名函数,并且可以捕获一定范围内的变量。lambda表达式的语法形式可简单归纳如下:
[ capture ] ( params ) opt -> ret { body; };
其中:
capture是捕获列表;params是参数表;opt是函数选项;ret是返回值类型;body是函数体。
因此,一个完整的lambda表达式看起来像这样:
auto f = [ ](int a) -> int { return a + l; };
std::cout << f(1) << std::endl; //输出:2
可以看到,上面通过一行代码定义了一个小小的功能闭包,用来将输入加1并返回。
在C++11中,lambda表达式的返回值是通过前面介绍的返回值后置语法来定义的。其实很多时候,lambda表达式的返回值是非常明显的,比如上例。因此,C++11中允许省略lambda表达式的返回值定义:
auto f = [ ] (int a){ return a + l;};
这样编译器就会根据return语句自动推导出返回值类型。
需要注意的是,初始化列表不能用于返回值的自动推导:
auto xl = [ ](int i){ return i; }; //OK: return type is int
auto x2 = [](){ return { 1, 2 }; }; //error:无法推导出返回值类型
这时我们需要显式给出具体的返回值类型。
另外,lambda表达式在没有参数列表时,参数列表是可以省略的。因此像下面的写法都是正确的:
auto f1 = [](){ return 1; };
auto f2 = []{ return 1; }; //省略空参数表
lambda表达式可以通过捕获列表捕获一定范围内的变量:
- []不捕获任何变量。
- [&]捕获外部作用域中所有变量,并作为引用在函数体中使用(按引用捕获)。
- [=]捕获外部作用域中所有变量,并作为副本在函数体中使用(按值捕获)。
- [=, &foo]按值捕获外部作用域中所有变量,并按引用捕获 foo变量。
- [bar]按值捕获bar变量,同时不捕获其他变量。
- [this]捕获当前类中的this指针,让 lambda表达式拥有和当前类成员函数同样的访问权限。如果已经使用了&或者=,就默认添加此选项。捕获this的目的是可以在lamda中使用当前类的成员函数和成员变量。
#include <iostream>
class A
{
int i_ = 0;
void func(int x, int y)
{
auto x1 = []{return i_; }; //error,没有捕获外部变量
auto x2 = [=]{return i_ + x + y; };
auto x3 = [&]{return i_ + x + y; };
auto x4 = [this]{return i_; };
auto x5 = [this]{return i_ + x + y; }; //error,没有捕获x和y
auto x6 = [this, x, y]{return i_ + x + y; };
auto x7 = [this]{return i_++; };
}
};
int main(void)
{
int a = 0;
int b = 1;
auto f1 = []{return a; }; //error,没有捕获外部变量
auto f2 = [&]{return a++; };
auto f3 = [=]{return a; };
auto f4 = [=]{return a++; }; //error,a是以复制方式捕获的,无法修改
auto f5 = [a]{return a + b; }; //error,没有捕获变量b
auto f6 = [a, &b]{return a + (b++); };
auto f7 = [=, &b]{return a + (b++); };
system("pause");
return 0;
}
从上例中可以看到,lambda表达式的捕获列表精细地控制了lambda表达式能够访问的外部变量,以及如何访问这些变量。需要注意的是,默认状态下lambda表达式无法修改通过复制方式捕获的外部变量。如果希望修改这些变量的话,我们需要使用引用方式进行捕获。
一个容易出错的细节是关于lambda表达式的延迟调用的:
int a = 0;
auto f = [=]{ return a; };
//按值捕获外部变量
a += 1;
//a被修改了
std: :cout<< f()<< std: :endl;
在这个例子中,lambda表达式按值捕获了所有外部变量。在捕获的一瞬间,a的值就已经被复制到f中了。之后a被修改,但此时f中存储的a仍然还是捕获时的值,因此,最终输出结果是0。如果希望lambda表达式在调用时能够即时访问外部变量,我们应当使用引用方式捕获。从上面的例子中我们知道,按值捕获得到的外部变量值是在 lambda表达式定义时的值。此时所有外部变量均被复制了一份存储在lambda表达式变量中。此时虽然修改lambda表达式中的这些外部变量并不会真正影响到外部,我们却仍然无法修改它们。
那么如果希望去修改按值捕获的外部变量应当怎么办呢?这时,需要显式指明lambda表达式为mutable:
int a = 0 ;
auto f1 = [=]{return a++; }; //error,修改按值捕获的外部变量
auto f2 = [=] () mutable { return a++; }; //OK,mutable
下面的代码
#include <iostream>
#include <vector>
#include <algorithm>
class CountEven
{
int& count_;
public:
CountEven(int& count) : count_(count){}
void operator()(int val)
{
if (!(val & 1)) //val % 2 == 0
++count_;
}
};
int main(void)
{
std::vector<int> v = { 1, 2, 3, 4, 5, 6 };
int even_count = 0;
std::for_each(v.begin(), v.end(), CountEven(even_count));
std::cout << "The number of even is " << even_count << std::endl;
system("pause");
return 0;
}
这样写既烦琐又容易出错。有了lambda表达式以后,我们可以使用真正的闭包概念来替换掉这里的仿函数,代码如下:
std::vector<int> v= {1,2,3,4,5,6 };
int even_count = 0;
for_each(v.begin(), v.end(), [&even_count](int val)
{
if(!(val & 1)) //val % 2 == 0
++even_count;
} );
std::cout << "The number of even is " <<even_count << std::endl;
lambda表达式比 std::bind的灵活性更好,也更为简洁。当然,这都得益于lambda表达式的特征,它可以任意封装出功能闭包,使得短小的逻辑可以以最简洁清晰的方式表达出来。
//查找大于5小于10的元素的个数
int count = std::count_if(coll.begin(), coll.end(), [](intx){ return x > 5 && x < 10;});
7 tupe元组
tuple元组是一个固定大小的不同类型值的集合,是泛化的std:pair。
先构造一个tuple:
tuple<constchar*, int>tp = make_tuple(sendPack, nSendSize); //构造一个tuple
这个tuple等价于一个结构体:
struct A
{
char* p;
int len;
};
用tuple<const char*, int>tp就可以不用创建这个结构体了,而作用是一样的。还有一种方法也可以创建元组,用std:tie,它会创建一个元组的左值引用。
auto tp = return std::tie(1, "aa", 2); //tp的类型实际是std::tuple<int& , string&, int&>
获取元组的值:
constchar* data = tp.get<0>(); //获取第一个值
int len = tp.get<1>(); //获取第二个值
还有一种方法也可以获取元组的值,通过std::tie解包tuple:
int x, y;
string a;
std::tie(x, a, y) = tp;
通过tie解包后,tp中3个值会自动赋值给3个变量。解包时,如果只想解某个位置的值时,可以用std::ignore占位符来表示不解某个位置的值。比如我们只想解第3个值:
std::tie(std::ignore, std::ignore, y) = tp;
还有一个创建右值的引用元组方法: forward_as_tuple。
std::map<int, std::string> m;
m.emplace(std::forward_as_tuple(10, std::string(20, 'a')));
它实际上创建了一个类似于std::tuple<int&&, std::string&&>类型的 tuple。我们还可以通过tuple_cat连接多个tupe:
std::tuple<int, std::string, float> t1(10, "Test", 3.14);
int n = 7;
auto t2 = std::tuple_cat(t1, std::make_pair("Foo", "bar"), t1, std::tie(n)) ;
n = 10;
print(t2) //(10, Test, 3.14, Foo, bar, 10, Test, 3.14, 10)