C++11新特性(3)
C++11引入了右值引用和移动语义,避免无谓的赋值,提高了程序的性能,同时,C++11引入了不定序容器,例如undored_map,标准库中的map容器插入元素的时候会自动排序,但是在不需要排序的场景下,这种额外的操作会影响程序的性能。而不定序容器在插入元素的时候,在不需要排序的情况下不会浪费额外的性能。
1.右值引用:
首先介绍什么是左值和右值:
左值是指表达式结束后依然存在的持久对象,右值是指表达式结束后就不存在的临时对象。其实区分左值和右值的简便方式是 :
看能不能对表达式取地址,如果能,则为左值,否则为右值,所有的具名变量和对象都是左值,右值不具名。
C++11中,右值由两个概念组成,一个是将亡值,另一个是纯右值,例如,非引用返回的临时变量,运算表达式产生的临时变量,原始字面量(int i=0, 0就是原始字面量), lambda等都是纯右值。将要被移动的对象,T&&的返回值,std::move返回值和转换为T&&的类型的转换函数的返回值。
&&的特性:
右值引用就是对一个右值进行引用的类型,右值不具名,所以只能通过引用的方式找到它,无论是声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不具有所绑定对象的内存,它只是一个别名。
-----------------------------------------------------------------------------------------------------------------------------------------------
C++move语义:
move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存拷贝,move与深拷贝是有本质的区别的。
std::move实际上并不能移动任何东西,它的唯一功能是将一个左值强制转换为一个右值引用,是我们可以通过右值引用使用该值。例如,一个对象中有一些指针资源或者动态数组,在定义对象的拷贝构造函数和重载赋值运算符的时候就不需要拷贝这些资源了(在前面的博客中岑详细介绍过这种方法),仅仅转移资源的所有者,将资源的拥有者改为被赋值者:
例如,假设一个临时容器很大,需要赋值给零一个容器:
int main()
{
// 直接赋值
std::vector<int> data; // 假设data很大
std::vector<int> temp = data;
//move移动语义
std::vector<int> temp = std::move(data);
}
如果不用move,拷贝的代价很大,效率很低。使用move几乎没有任何代价,只是转换了资源的所有权,实际上是将左值变成右值引用,然后应用move语义调用构造函数,就避免了拷贝。当一个对象的内部有大量的堆内存或者动态数组的时候,很有必要写move语义的拷贝构造函数和赋值运函数,避免无谓的深拷贝。实际上,C++中所有的容器都实现了move语义,方便我们实现性能优化。
emplace_back()减少内存拷贝和移动
emplace_back能就地通过参数构造对象,不需要拷贝或者移动内存,相比于push_back能够更好地避免内存的拷贝和移动,使容器插入元素的性能得到更大的提升。标准库的容器都添加了相应的emplace方法,所以应该尽量使用emplace来代替push,pop等操作。
例如:
// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <iostream>
#include <vector>
using namespace std;
// 定义结构
struct A
{
int x;
double y;
A(int a, double b) : x(a), y(b) {}
};
int main()
{
vector<A> v;
v.emplace_back(1, 1.2);
cout << v.size() << endl;
return 0;
}
emplace_back的用法:直接通过构造函数的参数就可以构造对象,因此,也要求对象有相应的构造函数,否则编译器会报错。
// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <iostream>
#include <vector>
#include <map>
#include <string>
using namespace std;
// 定义结构
struct Info
{
int year;
double money;
string name;
// 构造函数
Info(int y, double m, string n) : year(y), money(m), name(n)
{
cout << "Is constructed ! " << endl;
}
// 拷贝构造函数
Info(const Info& other) : year(other.year), money(other.money), name(std::move(other.name))
{
cout << "Is moved " << endl;
}
};
int main()
{
map<int, Info> m;
int a_year = 3;
double a_money = 78;
string a_name("zhangsdan");
cout << "use insert" << endl;
m.insert(std::make_pair(6, Info(a_year, a_money, a_name)));
cout << "use emplace " << endl;
m.emplace(6, Info(a_year, a_money, a_name)); // 使用emplace
vector<Info> v;
cout << "use emplace back" << endl;
v.emplace_back(a_year, a_money, a_name);
cout << "use push back" << endl;
v.push_back(Info(a_year, a_money, a_name));
return 0;
}
运行结果:
在map中,使用insert发生了一次构造和两次内存移动, 而使用emplace方法只发生了一次构造和一次内存移动。
在vector中,使用push_back发生了一次构造和两次移动,而使用emplace_back只发生了一次构造。
可以看到emplace对性能的提高,所以应该尽可能地使用emplace,但是前提条件是得有相应的构造函数。
C++11种泛型编程的一些特性:
type_traits----类型萃取:
type_traits提供了丰富的编译期计算,查询,判断,转换和选择的帮助类,在很多场合中会使用到这些特性。type_traits在一定程度上可以消除冗长的switch-case 或者if-else的语句,降低程序的圈复杂度,提高代码的可维护性。type_traits的类型判断功能,在编译期就可以检查出是否是正确的类型,以便能编写更加安全的代码。
简单的type_traits:
之前介绍过,在类型种定义编译期常量,有两种方法:
1. 通过static关键字
template<typename Type>
struct MyStruct
{
static const int value = 1;
};
2. 通过枚举的办法:
struct MyStruct
{
enum{value=1};
};
而在C++11中定义编译期常量,只需要从std::integral_constant派生:
template<typename Type>
struct MyStruct : std::integral_constant<int , 1>
{
};
int main()
{
MyStruct<int>::value;
return 0;
}
integral_constant的实现比较简单:
template<typename T, T v>
struct integral_constant
{
static const T value = v;
typedef T value_type;
typedef integral_constant<T, v> type;
operator value_type() { return value; }
};
类型判断的type_traits
用来检查某种模板类型是否为某种类型。通过这些traits可以获得编译期检查的结果:
例如:std::is_void, std::is_floating_point等等,具体的可以参考:https://en.cppreference.com/w/cpp/types,例如,常用的traits类型判断:
例如:
int main()
{
cout << "int: " << std::is_const<int>::value << endl;
cout << "const int: " << std::is_const<const int>::value << endl;
cout << "const int*: " << std::is_const<const int*>::value << endl;
return 0;
}
除了用于判断类型之外,还有用于判断两个类型之间的关系:
is_same用来在编译期判断两种类型是否相同:
int main()
{
cout << std::is_same<int, int>::value << endl; //1
cout << std::is_same<int, unsigned int>::value << endl; //0
cout << std::is_same<int, float>::value << endl; // 0
return 0;
}
is_base_of用来判断两种类型是否存在继承关系:前面的模板参数是基类
// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <iostream>
#include <type_traits>
using namespace std;
class A {};
class B : A {};
class C {};
int main()
{
cout << std::is_base_of<A, B>::value << endl;
cout << std::is_base_of<C, B>::value << endl;
return 0;
}
is_convertible: 判断前面的模板参数是否可以转换为后面的模板参数:
// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <iostream>
#include <type_traits>
using namespace std;
class A {};
class B : public A {};
class C {};
int main()
{
cout << std::is_convertible<B*, A*>::value << endl; // 1 可以向上转换
cout << std::is_convertible<A*, B*>::value << endl; // 0 不能向下转换
return 0;
}
用于类型转换的traits:
常用的型转换的traits包括:对const的添加和移除,对引用的添加和移除,对数组的修改,对指针的修改,如下图所示:
具体参考:https://en.cppreference.com/w/cpp/types
上述类型转换的具体用法如下所示:
// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <iostream>
#include <type_traits>
using namespace std;
int main()
{
//添加和移除const
cout << std::is_same<const int, std::add_const<int>::type>::value << endl;
cout << std::is_same<int, std::remove_const<const int>::type>::value << endl;
//添加和移除reference
cout << std::is_same<int&, std::add_lvalue_reference<int>::type>::value << endl;
cout << std::is_same<int&&, std::add_rvalue_reference<int>::type>::value << endl;
cout << std::is_same<int, std::remove_reference<int&>::type>::value << endl;
// 添加和移除pointer
cout << std::is_same<int*, std::add_pointer<int>::type>::value << endl;
cout << std::is_same<int, std::remove_pointer<int*>::type>::value << endl;
// 数组的维度:
int arr0[3];
int arr1[3][3];
int arr2[3][3][3];
cout << std::rank<decltype(arr0)>::value << endl; // 2
cout << std::rank<decltype(arr1)>::value << endl; // 返回数组的维度 3
cout << std::rank<decltype(arr2)>::value << endl;
// 移除数组的顶层维度
cout << std::is_same<int, std::remove_extent<int[]>::type>::value << endl;
cout << std::is_same<int[5], std::remove_extent<int[][5]>::type>::value << endl;
cout << std::is_same<int[5][5], std::remove_extent<int[][5][5]>::type>::value << endl;
cout << std::is_same<int, std::remove_all_extents<int[][5][5]>::type>::value << std::endl; // 移除所有维度
// 获取公公类型, 所有类型的
typedef std::common_type<unsigned int, unsigned char, short, int>::type Numeric_type;
cout << std::is_same<int, Numeric_type>::value << endl;
return 0;
}
当模板参数是引用类型,而在创建对象的时候,需要原始的类型,不能用引用类型,所以需要将可能的引用移除,而有时候需要添加引用,比如从智能指针中获取对象的引用时,可以看下面的一个例子:
// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <iostream>
#include <type_traits>
#include <memory>
using namespace std;
template<typename T>
struct MyStruct
{
private:
std::unique_ptr<U> m_ptr;
public:
typedef typename std::remove_reference<T>::type U; // 移除可能的引用 U是 T移除引用后的原始类型
MyStruct() : m_ptr(new U) {}
typename std::add_lvalue_reference<U>::type Get() const // typename std::add_lvalue_reference<U>::type相当于函数返回值的类型
{
// 返回值添加左值引用
return *m_ptr.get(); // 先获取原始指针
}
};
int main()
{
MyStruct<int> temp;
int a = temp.Get();
cout << a << endl;
return 0;
}
如果类型中带有cv符。则应该按照如下的方式进行处理:
例如:
template<typename T>
T* Create()
{
return new T();
}
如果类型T是一个带有c-v符的引用类型,则应该如何调用它
int* p = Create<const volatile int&>(); // 错误的调用方方法,编译不能通过
上面的方法是无法进行运行的,需要移除cv符和引用之后才能获取原始类型:
template<typename T>
typename std::remove_cv<typename std::remove_reference<T>::type>::type* Create()
{
typedef typename std::remove_cv<typename std::remove_reference<T>::type>::type U;
return new U();
}
上述的写法过于复杂,C++11提供了std::decay来简化代码的书写:
template<typename T>
typename std::decay<T>::type* Create()
{
typedef typename std::decay<T>::type U;
return new U();
}
对于普通的类型来说,decay用于解除和cv符,此外,decay还可以用于数组和函数,具体转换的规则和顺序如下所示:
第一步: 先移除T类型的引用,得到类型U,U定义为remove_reference<T>::type
第二步:如果is_array<U>为True,的修改类型为type_extent<U>::type*
第三步:否则,如果is_function<U>::value为true,修改类型为add_pointer<U>::type
第四步:修改类型为remove_cv<U>::type
所以,按照上述的规则,可以看到下面的转换:
typedef std::decay<int>::type A; //int
typedef std::decay<int&>::type A; //int
typedef std::decay<int&&>::type A; //int
typedef std::decay<const int&>::type A; //int
typedef std::decay<int(int)>::type A; //int(*)(int)
typedef std::decay<int[2]>::type A; //int*
根据条件选择traits:
std::coditional在编译时期根据一个判别式选择两个类型中的一个,和条件表达式的格式类似:
template<bool C, typename A, typename B>
struct Cond
{
};
通过这种方式,我们可以根据编译时期的判别式来选择类型,为动态选择类型提供了很大的灵活性,例如:
// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <iostream>
#include <type_traits>
#include <memory>
using namespace std;
int main()
{
typedef std::conditional<true, int, float>::type A;
typedef std::conditional<false, int, float>::type B;
typedef std::conditional<std::is_integral<A>::value, B, A>::type C;
typedef std::conditional<(sizeof(long long) > sizeof(long double)), long long, long double > ::type max_size_t;
cout << typeid(max_size_t).name() << endl;
return 0;
}
根据条件禁用或者启用某些类型traits
在《C++primer》中曾经介绍过,编译器在匹配重载函数的时候会匹配所有的重载函数,找到一个最精确匹配的函数。
例如:
void func(T*)
{
}
void func(T*)
{
}
func(2);
在匹配过程中,当匹配到void Func(T*)时,编译器会发现,将一个非0的整数用来替换T*是错误的,此时编译器并不会报错,而是会继续匹配其他的重载函数,直到找到最优的匹配。整个过程就不会报错,这个规则就是SFINAE(subsititution failure is not an error),即替换失败并非错误。
std::enable_if更具SFINAE实现根据条件选择重载函数:
// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <iostream>
#include <type_traits>
#include <memory>
using namespace std;
template<typename T> // 根据std::enable_if()确定模板的参数类型
typename std::enable_if<std::is_arithmetic<T>::value, T>::type foo(T a) // 参数为数字类型
{
return a;
}
int main()
{
cout << foo(2) << endl;
cout << foo(3.2) << endl;
cout << foo("hello") << endl; // error
return 0;
}
利用std::enable_if对模板函数的参数做了限定,如果参数类型是非arithmetic(数字)类型的则不能通过编译。
除此之外,std::enable_if还可以用于模板定义,类模板的特化和入参类型的限定,例如:
// 对函数的然会值类型进行分类,分为arimethmetic和非arimethic类型
template<typename T> // 根据std::enable_if()确定模板的参数类型
typename std::enable_if<std::is_arithmetic<T>::value, int>::type foo(T a) // 参数为数字类型
{
cout << a << endl;
return 0;
}
template<typename T> // 根据std::enable_if()确定模板的参数类型
typename std::enable_if<!std::is_arithmetic<T>::value, int>::type foo(T& a) // 参数为数字类型
{
cout << typeid(a).name() << endl;
return 1;
}
std::enable具有限定模板参数的作用,因此,可以使用std::enable_if在编译期间检查输入的模板参数是否有效,从而可以避免直到运行期才能发现的错误。std::enable_if可以实现强大的重载机制,只有参数不同才能实现重载,如果只有返回值不同是不能进行重载的。可以利用std::enable_if来提高程序的可维护性和降低圈复杂度:
关于全复杂度的定义:
圈复杂度作为衡量代码质量的一个指标,可以使用下面的方法来度量复杂度:
1. 从函数第一行开始,一直往下看程序
2. 一旦遇到以下关键词或者同类的词就+1 , if, while , for, repeat, and , or
3. case语句中的每一种情况+1
// 定义一个字符串转换函数
template<typename T>
string ToString(T t)
{
if (typeid(T) == typeid(int) || typeid(T) == typeid(double) || typeid(T) == typeid(float))
{
std::stringstream ss;
ss << t;
return ss.str();
}
else if (typeid(T) == typedef(string))
{
return t;
}
// 圈复杂度大
}
// 通过std::ennable_if来降低圈复杂度
template<typename T>
typename std::enable_if<std::is_arithmetic<T>::value, string>::type ToString(T& t)
{
return std::to_string(t);
}
template<typename T>
typename std::enable_if<!std::is_arithmetic<T>::value, string>::type ToString(T& t)
{
return t;
}
------------------------------------------------------分割线-------------------------------------------------------------
可变参数模板
C++11增强了模板功能,在C++1之前,类模板和函数模板只能含有固定数量的模板参数,c++11中的新特性可变参数模板允许模板定义中包含0到人一个模板参数。可变参数模板和普通模板的语义是一样的,只是写法上稍有不同。需要在typename的后面加上省略号...
省略号的作用:
1. 声明一个参数包,这个参数包中可以包含0到任意个模板参数
2. 在模板定义的右边,可以将参数包展开成一个一个独立的参数
例如:
template<typename... T> // T是一个参数包
void func(T... args) // 将参数包展开成一个一个独立的参数
{
}
参数包的展开方式有两种
1. 通过递归的模板函数将参数包展开
2. 通过逗号表达式和初始化列表的方式展开参数包
a. 递归方式:
// 定义递归终止函数
void print()
{
cout << "empty" << endl;
}
// 递归展开函数
template<typename T, typename ...Args>
void print(T head, Args... rest)
{
cout << "parameter: " << head << endl;
print(rest...); // 递归调用
}
输出结果:
2. 逗号表达式,初始化列表
这是一种通过非递归的方式展开参数包的方法,更加简单,需要借助逗号表达式和初始化列表。
// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <iostream>
#include <type_traits>
#include <memory>
#include <string>
using namespace std;
template<typename T>
void printarg(T t)
{
cout << t << endl;
}
template<typename ...Args>
void expand(Args ...args)
{
int arr[] = { (printarg(args), 0)... };
}
int main()
{
expand(1, 2, 3, 4, 5);
return 0;
}
逗号表达式是展开这种参数包的关键,都阿红表达式会按照顺序进行执行,例如:
d = (a=b, c)
b会先赋值给a, 接着括号中的逗号表达式返回c的值,因此d = c
expand()函数中的表达式也是按照这个顺序进行的,(printarg(args, 0)),先输出参数,然后再返回0,所以前面需要声明一个数组。还用到了C++中的初始化列表,{(printarg(args, 0))...}会展开为{printarg(args1, 0)), printarg(args2, 0)), printarg(args3, 0)) , printarg(args4, 0))},所以最后返回的是一个数组。
还可以将上面的程序改进为更加简洁的形式:
template<typename ...Args>
void expand(Args... args)
{
std::initializer_list<int>{(printarg(args), 0)...};
}
function_traits
很多时候需要获取函数的实际类型,返回类型,参数个数以及参数类型,因此,需要一个function_traits来获取这些信息,而且这个function_traits能够所有函数语义类型的信息,即可以获取普通函数,函数指针,std::function, 函数对象和成员函数的函数类型,返回类型,参数个数以及参数类型。
例如,定义一个函数:
int func(int a, string b) {}
int main()
{
// 获取函数类型
cout << std::function_traits<decltype(func)>::function_type << endl; // int __cdecl(int, string)
// 获取函数返回值类型
cout << std::function_traits<decltype(func)>::return_type << endl; // int
// 获取函数参数个数
cout << std::function_traits<decltype(func)>::arity << endl; // 2
// 获取函数第一个参数类型
cout << std::function_traits<decltype(func)>::arg_type<0> << endl; // int
// 获取函数第2个参数类型
cout << std::function_traits<decltype(func)>::arg_type<1> << endl; // string
return 0;
}