C++新特性
emplace_back和push_back,完美转发
两者都是想stl添加元素。
前者直接在stl末尾构造对象,后者是先构造一个临时对象,然后再移动甚至拷贝过去。
当然前一句话只是这两个api的“目的”
实际使用的时候却不一定会按照意愿来。
- 有关两者对性能上的优化,可以从三个方面入手
——构造,移动,复制
根据两者不同的使用环境,也会做出不一样的表现
——传左值,传右值,传对象,传类型,传引用。。。。。。
- 如果是对象,也会更具对象是否包含各类构造函数产生不同的表现
一一举例子
vector<int> vec;
vec.push_back(1);
vec.emplace_back(1);
简单类型,两者没有差异,但是内部调用区别却很大。这里循序渐进留到最后说。
A a(1);
vector<A> vec;
vec.push_back(a);
vec.emplace_back(1);
emplace_back
不能直接接受对象做参数,因为他要直接接受对象的构造函数参数,然后在容器内直接进行构造。
push_back做的事:
- 使用拷贝构造函数,通过传递过来的对象a,拷贝出另一个临时对象 +拷贝
- 将拷贝出的临时对象插入容器尾部
emplace_back做的事:
- 使用构造函数,通过传递过来的参数,直接在容器内构造出一个对象 +构造
如果涉及深拷贝操作,那么拷贝可能会比构造性能消耗更大。
这里的主流观点都是emplace_back
进行了性能优化。
因为要注意到的是,虽然单看后面两句,可能还存有到底是拷贝还是构造更消耗性能的争议,但是push_back执行这一语句的前提是已经构造出一个对象了(+构造),所以如果只是有参数,那么用emplace_back肯定会更好点。
当然也有说法是如果已经存在对象,在无视对象构造的前提下,最好使用push_back,不过我认为具体问题具体分析,根据需求实际测试性能损耗会更好些。
A a(1);
vector<A> vec;
vec.push_back(std::move(a)); //move函数将类型转换为右值
vec.emplace_back(1);
push_back有新重载(C++11之后):
//可以看到,里面都是调用的emplace_back函数
void push_back(const _Ty& _Val)
{ // insert element at end, provide strong guarantee
emplace_back(_Val);
}
void push_back(_Ty&& _Val)
{ // insert by moving into element at end, provide strong guarantee
emplace_back(_STD move(_Val));
}
注意C++11引入右值之后大部分源码发生变化
如果传递的是左值(由于使用常量引用参数进行了修饰则可以被emplace_back传递),他会通过对象的参数类型来重新构造函数。之后emplace_back由于收到的是左值,所以调用的是拷贝构造函数,重新对对象进行构造(+拷贝)。
如果传递的是右值,则调用移动构造函数进行构造(+移动),很明显,移动要比重新构造性能上好很多。这里如果原对象没有移动构造函数,则会调用拷贝构造函数,白白浪费性能。
值得注意的是,使用emplace_back并不会总是执行移动构造,若参数为左值,则拷贝构造,右值则移动构造。
所以说上面的例子,如果更换成右值,那么push_back会被优化成emplace_back:
A a(1);
vector<A> vec;
vec.push_back(a);
vec.emplace_back(1);
vector<A> vec;
vec.push_back(a()); //这里push_back本身的调用和emplace_back一致只调用移动构造,但是不能理解成做了性能优化,因为仍然需要进行a()本身的构造。
vec.emplace_back(1);
完美转发
这里会出现一个问题,函数调用的时候,函数参数很容易会由于参数绑定而成为一个左值(哪怕传递的是右值)。
template<typename T>
void solve(T && v){
//v接受任何参数传递
}
很尴尬的是,哪怕确实传入了右值,在函数内部一旦使用v,v立马就会变为左值。
以这个例子看,emplace_back传入右值又是如何实现保持类型不变呢。
template<typename T>
void solve(T && v){
_solve(v); //左值
_solve(std::forward<T>(v)); //v是什么类型那就是什么类型
_solve(std::move(v)) //move强行转换为右值
}
看看forward函数的原理:
template <typename T>
T&& forward(typename std::remove_reference<T>::type& param)
{
return static_cast<T&&>(param);
}
template <typename T>
T&& forward(typename std::remove_reference<T>::type&& param)
{
return static_cast<T&&>(param);
}
remove_reference意思就是去掉引用的参数类型。
这里假如T类型是int、int&或int&&类型。最终param本身的类型都会变为int,但会将T原封不动传入。
如果传入的是左值,也就是调用重载的第一个forward,此时T翻译为int&
,最终参数类型转化为int& &&
。
如果传入的是右值,也就是调用重载的第二个forward,此时T翻译为int&&
,最终参数类型转换为int&& &&
。
引用折叠规则:
int & &
折叠为int&
int & &&
折叠为int&
int && &
折叠为int&
int && &&
折叠为int &&
即对
T&&
来说左值引用折叠为左值引用,右值引用折叠为右值引用
所以最终,forward会返回对应T的类型,这个就叫完美转发。
C++ Primer:forward必须通过显式模板实参来调用,不能依赖函数模板参数推导
要点就是函数传入的右值会变为左值,但是forward函数直接无视参数本体,而是直接将参数类型传入,然后重新和参数本体拼装(强制类型转换)后返回出去。
使用引用折叠的原因——万能引用:
如果直接传递右值,则T会直接推导成为非引用类型。非引用类型即可以是左值也可以是右值,这里存在一个二义性,也就是说非引用类型返回出去,具体是左值还是右值可能取决于绑定的对象
此时加上&&,则非引用类型转换为右值引用,而如果传入的是左值,则通过折叠转换为左值引用。
其中
T&&
中的T只是代表”需要被推导的类型“,所以如果是auto&&
其实也是可以算万能引用的。
回到emplace_back上,无论是外界调用引入参数,还是emplace_back的内部调用,都是可以使用完美转发直接传递左值或者右值。
萃取
顾名思义,即将迭代器iterator所指向的元素type萃取出来。
constexpr
旨在支持编译时常量表达式的计算
被constexpr函数,其值会在编译期确定。 需要函数体是在编译器能决定的,特别是返回值,需要让他在编译器就能确定好值。 而且返回值需要是单一返回值。
constexpr int Len(int a,int b){
return a + b;
}
int main(){
constexpr int array[Len(1,2)];//不会报错,可以看出1+2必定是编译器能判断出结果的
return 0;
}
#define不受名字空间限制,编译时整个编译单元都有效,所以可能发生冲突。 建议使用constexpr或者模板来取代。值得说的是,UE4是将一个模块里的所有cpp都合并成一个编译单元进行编译,导致所有cpp里面的#define都冲突了。另一个解决方式是使用完之后使用#undef来解决问题。
noexcept
在函数后接这个关键字,指函数中不会发生异常,编译器不会捕捉异常,如果真出现直接中断程序。 只有移动构造函数使用noexcept声明,vector扩容的时候才会采用移动构造 移动构造函数、析构函数、swap函数、内存释放函数都建议适用noexcept
auto推导规则
- 如果初始化表达式是引用,则去除引用语义;
- 如果初始化表达式为const或volatile(或者两者兼有),则除去const/volatile语义;
- 初始化表达式为数组时,auto关键字推导类型为指针。 也就是说auto表示不了引用和const,必须手动配合变更:
- auto x,推导时会忽略引用和const/volatile
- auto& x,推导时会保留const/volatile,最终一定是左值引用
- auto* x,推导时会保留const/volatile,最终一定是指针
- auto&& x,万能引用,推导时会保留const/volatile和左值/右值引用特性
variant
共同体union,其包含的类型都是基本类型,因为union不会调用元素的析构函数构造函数,甚至难以直接得知当前使用的类型是什么。 这就导致了实际开发中尽量不会使用union,就算使用也不会使用复制对象类型。 variant就是C++17之后的模拟union的标准库。
#include <variant>
#include <string>
#include <iostream>
int main() {
std::variant<int, double, std::string> v;
return 0;
}
v = 10; // 存储int类型的值
v = 3.14; // 存储double类型的值
v = "hello"; // 存储std::string类型的值
std::cout << std::get<int>(v) << std::endl; // 获取存储的int类型的值
std::cout << std::get<double>(v) << std::endl; // 获取存储的double类型的值
std::cout << std::get<std::string>(v) << std::endl; // 获取存储的std::string类型的值
std::visit([](auto&& arg) {
std::cout << arg << std::endl;
}, v);
- 与传统union比起来variant更加类型安全
- 内置机制来管理“活跃”成员,不需要手动储存额外空间,来记录当前类型
- 访问接口get()与visit()
- 支持复制与销毁
decltype
和auto一样作用的新关键字,在编译期自动推导变量类型,和auto不同的是,auto需要等号右边的变量至少已经被初始化了,而decltype不做要求。
decltype(exp) varName;
更具exp表达式来判断类型,exp可以是变量,也可以是左值右值,也可以是函数(根据返回值推导),唯独不能是void类型。 如果exp被括号包围,那么推导类型为exp类型的引用。 使用场景:
- 函数返回值
- 类的非静态成员(无法使用auto)