通常字面量是右值,但字符串字面量是左值
常量左值引用的作用
常量左值引用 不仅可以引用左值,还能引用右值:const int &x = 11;
而非常量左值引用 只能引用左值
class X
{
public:
X(){}
X(const X&){}
X& operator = (const X&){return *this;}
};
X make_x()
{
return X();//返回右值,这里发生拷贝构造,恰好const X&是常量左值引用,可以引用右值
}
一旦使用常量左值引用,就无法再修改该对象的内容了
右值引用的作用
右值引用可以延长右值的生命周期。右值引用不能引用左值
使用右值引用可以减少调用拷贝构造(实际上被优化过优化都一样,加命令行参数 -fno-elide-constructors关闭优化才能看到效果)
g++ test1.cpp -fno-elide-constructors
#include <iostream>
class X
{
public:
X(){
std::cout<<"构造"<<std::endl;
}
~X(){
std::cout<<"析构"<<std::endl;
}
X(const X&){
std::cout<<"拷贝"<<std::endl;
}
X& operator = (const X&){
return *this;
}
};
X make_x()
{
return X();//返回右值,这里发生拷贝构造,恰好const X&是常量左值引用,可以引用右值
}
int main()
{
//1.发生2次拷贝构造才得到了x1
X x1 = make_x();
//2.使用右值引用减少拷贝
X&& x2 = make_x();
}
-
泛左值:左值、将亡值
-
右值:将亡值、纯右值
-
将亡值的产生途径
- 使用static_cast 将 泛左值 转为 右值引用
- 临时量实质化:已知临时对象是纯右值,当要访问该对象的成员时,该临时对象实质化为将亡值
-
怎么将左值数据转移到另一个左值:
前面说到,右值引用不能引用左值。但可以先将其转为将亡值再来做。
int i = 0;
int &&j = static_cast<int &&> (i);X &&a = X();//a是右值引用 X b = a;//拷贝 X c = static_cast<X &&> (a);//移动 //此后不能再使用a了
注意:右值引用是左值
-
传入临时对象时,函数内部使用上述技巧的好处:
-
常规做法:
//接收右值引用,而右值引用 可引用 右值, 不可引用 左值。右值引用是左值。因此这里只能传右值 void test(X &&a) { //a是左值 X b(a);//没有必要的拷贝,a是一个将亡值传进来的,这里直接move更好 } int main() { //传入将亡值 test(X());//无论实参是左值还是右值,形参一定是左值 return 0; }
-
改进做法:
//接收右值引用,而右值引用 可引用 右值, 不可引用 左值。右值引用是左值。因此这里只能传右值 void test(X &&a) { //a是左值 X b(static_cast<X &&> (a));//a被强转为右值,就会调用移动构造来创建b,而不是拷贝 } int main() { //传入将亡值 test(X());//无论实参是左值还是右值,形参一定是左值 return 0; }
-
c++11提供了左值转右值的方法,不需要自己写static_cast了
std::move(a)即可转为右值引用
-
-
引用折叠规则
发生引用折叠时,任一引用为左值引用 则 结果为左值引用。否则为右值引用。
-
实现完美转发:调用std::forward(t)或者写static_cast进行强转
#include <iostream> using namespace std; void process(int & i) { cout<<"左值引用的函数"<<endl; } void process(int && i) { cout<<"右值引用的函数"<<endl; } template <typename T> void myfun(T && t) { //不能直接process(t),否则无论t是int还是int&,都只能调process(int &) //应当将t强转为T &&,即多折叠上&&,这样对于int,就恢复成了右值引用;对于int&,就保持不变 process(static_cast<T &&> (t)); // process(std::forward<T>(t)); } int main() { int a=3; myfun(a);//传入a,T被推导为int & myfun(2);//传入2,T被推导为int return 0; }
总结
问:说一个c++11提出的非常重要的概念
右值引用:用来延长右值的生命周期,还实现了与之相关的移动语义和完美转发,许多常用容器都已经实现了移动构造和移动赋值
变长模板参数与折叠表达式
-
要求1:实现变长参数求和
c++17中 …有两种展开方式,一种是
return (ts + ...)
以ts自身开始展开,另一种是return (0 + ... + ts)
指定一个初始值,然后展开。这里示例使用+ … ,实际上还可以有* …,甚至逗号 …展开。#include <iostream> template<class ...Ts> auto func(Ts ...ts) { //return (ts + ...)等价于return (1+2+3) return (0 + ... + ts);//这里写上0是为了有一个初始值,否则func()为空参数时会报错 } int main() { std::cout<<func()<<std::endl; std::cout<<func(1,2,3)<<std::endl; return 0; }
小细节:如果这样调用func((1,2,3)),会返回3,因为(1,2,3)是逗号表达式,表达式结果是其中的最后一个元素值
-
要求2:想要ts[0]、ts[1]这样遍历打印参数
使用逗号表达式,把()当作{},把逗号当作分号使用,相当于一个for循环。例如(myprint(ts), …)就会让参数依次执行myprint。
#include <iostream> template<class T> void myprint(T a) { std::cout<< a; } template<class T0, class ...Ts> auto func(T0 t0, Ts ...ts) { myprint(t0); ((myprint(","), myprint(ts)), ...); myprint("\n"); } int main() { func(1,2,3);//1,2,3 return 0; }
如果是在c++11中,无法使用逗号…来展开,可以像下面这样写函数模板。
auto func(){} template<class T0, class ...Ts> auto func(T0 t0, Ts ...ts) { myprint(t0); myprint(","); func(ts...);//1,2,3, }
如果想要最后一个参数时不多打印一个逗号,要么利用c++17将myprint(“,”)写在if constexpr分支里,要么使用SFINAE来实现。下面是使用SFINAE(匹配失败不是错误)来实现的
#include <iostream> #include <numeric> template<class T> void myprint(T a) { std::cout<< a; } auto func(){ myprint("\n"); } template<class T0, class ...Ts, std::enable_if_t<sizeof...(Ts) == 0, int> = 0> auto func(T0 t0, Ts ...ts) { myprint(t0); func(ts...); } template<class T0, class ...Ts, std::enable_if_t<sizeof...(Ts) != 0, int> = 0> auto func(T0 t0, Ts ...ts) { myprint(t0); myprint(","); func(ts...); } int main() { func(1,2,3);//1,2,3 return 0; }
实际上还可以结合lambda表达式使用
([&](){ myprint(","); myprint(ts); }(), ...);
-
要求3:怎么找到参数类型的共同类型(例如int和float共同类型是float,Cat和Animal共同类型是Animal)
方法1:using T = std::common_type_t<Ts...>;
方法2:利用decltype
using type = decltype(1?int():double());得到的type是double,因为?后面的值在比较前要先提升到同一类型,这里提升为double。
例如:下面的res类型为double
double x = 3.5;
int y = 4;
auto res = (0 ? x : y);//这里使用三目运算符配合decltype得到共同类型。 //之所以不使用decltype(T1()+T2()), //是因为T1和T2类型可能不是数值类型,可能是子类与父类的关系。 template<class T1, class T2> struct common_type { using type = decltype(0?T1():T2()); //如果T1和T2没有构造函数,可以使用declval凭空构建 //using type = decltype(0?std::declval<T1>():std::declval<T2>()); }; int main() { using what = typename common_type<const char *, char *>::type; return 0; }
拓展1:多使用模板偏特化来获取多个类型的共同类型
#include <iostream> #include <typeinfo> //求两个类型的共同类型 template<class T1, class T2> struct common_type_two { using type = decltype(0?std::declval<T1>():std::declval<T2>()); }; //主模板 template<class ...Ts> struct common_type { }; //特化:结束条件,只有一个类型时,直接返回自身 template<class T0> struct common_type<T0> { using type = T0; }; //特化 template<class T0, class T1, class ...Ts> struct common_type<T0,T1,Ts...> { using type = typename common_type_two<T0,typename common_type<T1,Ts...>::type>::type; }; int main() { using what = typename common_type<double, int, float>::type; return 0; }
拓展2:使用编译期函数搭配条件编译来实现
#include <typeinfo> #include <iostream> template <class T0, class ...Ts> constexpr auto get_common_type(T0 t0, Ts ...ts) { if constexpr (sizeof...(Ts) == 0){ return t0; } else{ return 0 ? t0 : get_common_type(ts...); } } int main() { using what = decltype(int(), double(), float()); return 0; }
拓展2的问题:如果是想得到子类和父类的共同类型,不能使用std::declval来凭空构造,因为这里是真的要调用构造函数得到对象来传递参数到get_common_type。
拓展2的解决:但我们实际上需要的根本不是对象t0,而是它的类型,因此可以使用dummy(c++20才有),下面自己实现了一个dummy,T类型没有构造函数,但dummy可以有,因此可以传递dummy类型的变量。只需要最后调用dummy提供的返回T类型凭空构造的方法,对其decltype,就能获得最终T类型。#include <typeinfo> #include <iostream> template <class T> struct dummy { //这里自己定义的declval直接调用std库的declval进行凭空构造,外层decltype一下就能得到T类型 static constexpr T declval() { return std::declval<T>(); } }; template <class T0, class ...Ts> constexpr auto get_common_type(dummy<T0> t0, dummy<Ts> ...ts) { if constexpr (sizeof...(Ts) == 0){ return t0; } else{ //t0.declval()是dummy<Animal> return dummy< decltype(0 ? t0.declval() : get_common_type(ts...).declval()) >(); } } struct Animal {}; struct Cat:Animal { Cat(Cat &&) = delete;//删除移动,则拷贝与默认构造都没了 }; int main() { using what = decltype(get_common_type(dummy<Animal>(), dummy<Cat>()).declval()); return 0; }
-
要求4:想要变长参数类型全是int类型
只需要使用c++20里的requires结合变长模板参数。如果想要能转换成int的都可以,那么将下面的is_same_v改为is_convertible_v
template <class ...Ts> //为了避免无参数时报错,不要写成requires((std::is_same_v<Ts, int> && ...)) requires((true && ... && std::is_same_v<Ts, int>)) void myvec(Ts ...ts) {}
-
要求5:判断类型T0 是否为 …Ts中的其中一个类型
即T0要与…Ts进行 “或” 比较,使用std::is_same_v<T,Ts>即可比较,再搭配上变长模板参数…
template<class T, class ...Ts> struct is_same_any { static constexpr bool value = (false || ... || std::is_same_v<T,Ts>); };
-
要求6:之前求共同类型时,需要把各种类型(int float double)等一起传来传去,不如写成tuple类型
//1.定义tuple类型 using Tup = std::tuple<int, float, double>; //2.把之前的common_type结构体用tuple包装一遍,就可以这样使用了 //using what = tuple_apply_common_type<Tup>::type; //主模板 template<class Tup> struct tuple_apply_common_type {}; //特化 template<class ...Ts> struct tuple_apply_common_type<std::tuple<Ts...>> { //直接调用之前写的求共同类型 using type = typename common_type<Ts...>::type; };
-
variant的使用
#include <variant> int main() { //1.获知当前类型在variant的索引(从0开始) // std::variant<int, std::string, double> v = 3.5;//如果没赋值,默认为第一个类型 std::variant<int, std::string, double> v {std::in_place_index<2>,2.5};//明确写出要启用的数据类型 // std::variant<int, std::string, double> v {std::in_place_type<double>, 2.5};//与上面等价 std::cout<< v.index() <<std::endl;//2 //2.get获取值 std::cout<< std::get<double>(v) <<std::endl;//2.5,如果写成了get<int>会报错std::bad_variant_access //3.get_if获取值,得到的是指针,如果类型不正确会返回nullptr,使得有可以在访问前进行安全判断 int* p = std::get_if<int>(&v);//get_if只接受指针 std::cout<< (p == nullptr) <<std::endl;//安全判断:得知p为空指针 //4.检查当前variant是不是int类型 std::cout<< std::holds_alternative<int>(v) <<std::endl;//0 不是 //5.可切换的类型个数 std::cout<< std::variant_size_v<decltype(v)> <<std::endl;//3 //6.访问 std::visit([](auto && arg){ std::cout<<arg<<std::endl;//2.5 },v); //打印nh yx std::visit([](const auto &t){std::cout<<t<<" yx"<<std::endl;},std::variant<int, float, std::string> ("nh")); return 0; }
-
tuple的使用
//1.创建元组 std::tuple<int,double> a1(2, 2.5); //2.访问与修改 std::get<0>(a1) = 3;//修改 std::cout<< std::get<0>(a1) <<std::endl;//3 //3.元组的元素个数 std::cout<< std::tuple_size<decltype(a1)>::value <<std::endl;//2 //4.解包 int m; double n; std::tie(m,n) = a1; //5.获取元素类型 using what = std::tuple_element<0, decltype(a1)>::type;//int //元组拼接 std::tuple<float, char> a2; auto res = std::tuple_cat(a1, a2);//std::tuple<int, double, float, char> res
-
要求7:tuple类型的连接
template<class Tup1, class Tup2> struct tuple_cat { }; template<class ...T1s, class ...T2s> struct tuple_cat<std::tuple<T1s...>,std::tuple<T2s...>> { using type = std::tuple<T1s..., T2s...>; }; int main() { using what = tuple_cat<Tup1, Tup2>::type; return 0; }
-
要求8:tuple的push_front
template<class T1, class T2> struct tuple_push_front{}; template<class T1, class ...T2s> struct tuple_push_front<T1, std::tuple<T2s...>> { using type = std::tuple<T1, T2s...>; }; //使用 using what2 = tuple_push_front<char, Tup1>::type;
-
要求9:获取tuple的第一个索引类型
方式1:使用std::tuple_element_t获取指定下标的索引类型
template<class T> struct tuple_getfirst {}; template<class ...Ts> struct tuple_getfirst<std::tuple<Ts...>> { using type = std::tuple_element_t<0,std::tuple<Ts...>>; };
方式2:自己实现
template<class T>//如果tuple是空的,就会落入这里,因此其实可以在这里设置一个默认值 struct tuple_getfirst {}; template<class T0, class ...Ts> struct tuple_getfirst<std::tuple<T0, Ts...>> { using type = T0; };
-
要求10:实现tuple_element获取任意下标的索引类型
利用类模板递归#include <tuple> //主模板 template<int index, class T> struct tuple_element {}; //特化:index为0时 template<class T0, class ...Ts> struct tuple_element<0, std::tuple<T0, Ts...>> { using type = T0; }; //特化 template<int index, class T0, class ...Ts> struct tuple_element<index, std::tuple<T0, Ts...>> { using type = typename tuple_element<index-1, std::tuple<Ts...>>::type; }; int main() { using Tup1 = std::tuple<int,float,char>; using what = tuple_element<1,Tup1>::type;//float return 0; }
-
要求11:判断tuple里的类型是不是全为integral(int char short)
利用c++17的…折叠表达式template<class T> struct tuple_all_integral {}; template<class ...Ts> struct tuple_all_integral<std::tuple<Ts...>> { static constexpr bool value = (true && ... && std::is_integral_v<Ts>); };