0%

C++新特性

# C++新特性

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)