请你详细介绍一下C++11中的可变参数模板、右值引用和lambda这几个新特性。
在C++11中展开参数包不一定需要递归展开。可以利用一些小特性来展开,比如说例子中的print
函数:
template <class ...Args> void print(Args&& ...args) { int dummy[] = { ((std::cout << std::forward<Args>(args) << ','), 0)... }; }
这是利用了逗号表达式的特性,最终dummy
数组中所有元素都是0,但是输出也都顺序执行了。其顺序由list initialization保证(list initialization也是C++11新特性。其中出现的表达式,其求值顺序按其出现顺序排列,也即从左至右顺序求值)。这个未使用的数组正常的编译器都会将其优化掉,不会占用空间。如果实在担心的话……
struct dummy { template <class ...Ts> dummy(Ts&& ...ts) {} }; template <class ...Args> void print(Args&& ...args) { dummy d{ ((std::cout << std::forward<Args>(args) << ','), 0)... }; }
如果不需要保证顺序,可以用一个变参函数来接收所有的表达式(函数参数虽然是从右至左压栈,但参数的求值顺序不确定)
template <class ...Ts> void do_nothing(Ts&& ...ts) {} template <class ...Args> void print(Args&& ...args) { do_nothing(((std::cout << std::forward<Args>(args) << ','), 0)...); }
这种方式只适用于对所有参数包中的参数执行统一的操作,比如说对每个参数执行一个函数。如果你需要求和这类操作的话,还是要递归展开。不过C++17引入了fold expression,会将其简化很多:
// auto推导函数返回值类型,C++14特性 template <class ...Args> auto sum(Args&& ...args) { return 0 + ... + args; // 从左加到右 // 或者 return args + ... + 0; // 从右加到左 }
每个lambda对应一个你写不出名字的闭包类型(但是编译器知道),哪怕代码是一样的。这个闭包类型重载过operator()
。你甚至可以继承一个lambda的类型(虽然写不出来,但可以decltype
求出来)。可继承这个特性能够写一个配合C++17的std::variant
使用的工具类出来。
此外闭包类型内还含了一个向函数指针转换的自定义转换函数,因此不带capture的lambda可以转换成对应的函数指针,比如说[](int a, int b) { return a + b; }
可以转换成int(*)(int, int)
。转换的时候在lambda前面加一个+
就好(这是一元操作符加号)
int (*add)(int, int) = +[](int lhs, int rhs) { return lhs + rhs; };
C++14额外强化了lambda,允许其参数类型自动推导(每个auto
对应一个模板类型参数),也可以包含变参参数包:
auto add = [](const auto &lhs, const auto &rhs) { return lhs + rhs; }; auto var_add = [](auto&& ...args) { auto sum = // whatever ; return sum; };
此时相当于闭包类型里的operator()
是模板函数。
关于capture,C++11可以指定全体按引用捕获(&
),全体按值捕获(=
),单独按引用捕获(&name
),单独按值捕获(name
),按引用捕获当前对象(this
,需要lambda在成员函数内定义,不然不存在this
)。C++14允许你在capture中初始化一个只在lambda内生效的值/引用变量,具体和定义一个变量类似,只是去掉类型。比如[v = std::vector<int>{{1, 2, 3}}]
就给lambda添加了一个v
,其类型是std::vector<int>
,内容为1, 2, 3
。C++17允许你按值捕获当前对象(*this
)。
不指定mutable
的lambda,它的operator()
是const成员函数,因此所有按值捕获的capture是不能修改的。
单独的右值引用其实没有用,也没有额外生成任何代码。真正的移动语义需要右值引用配合右值重载函数,移动构造函数,移动赋值来起作用。
std::move
的实现相当简单,就是
template <class T> typename std::remove_reference<T>::type&& move(T &&t) { return static_cast<typename remove_reference<T>::type&&>(t); }
直接把T
转换成T&&
。(要remove_reference
是因为T
可能不是T
,还可能是T&
)
std::forward
由于引用折叠的存在,似乎可以实现成这个样子:
template<typename T> T&& std::forward(T&& param) { return static_cast<T&&>(param); }
写起来就是std::forward(rvalref)
,但实际上不行。右值引用绑定之后,延长了右值的生命周期,右值引用变量在后续使用中会被视作左值引用,如此实现的std::forward
在任何情况下产生的都是左值引用。解决办法就是强迫手动指定模板参数T
,并分别重载左值和右值引用情况。
template <class T> T&& forward(typename std::remove_reference<T>::type& t) { return static_cast<T&&>(t); } template <class T> T&& forward(typename std::remove_reference<T>::type&& t) { static_assert(!std::is_lvalue_reference<T>::value, "Can not forward an rvalue as an lvalue."); return static_cast<T&&>(t); }