欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

C++ 你应该考虑置入操作(emplace)而非插入操作————C++2.0知识补充

程序员文章站 2022-03-22 21:35:04
...

C++2.0知识补充

1 某些情况下考虑置入而非插入

1.1 拥有置入操作的容器

  • 1,emplace_back可用于任何支持push_back的标准容器;
  • 2,emplace_front可用于任何支持push_front的标准容器;
  • 3,任何支持插入操作(除了std::forward_liststd::array以外的所有标准容器)都支持置入操作;
  • 4,关联容器提供了emplace_hint来补充带有hint迭代器的insert函数;
  • 5,std::forward_list也有emplace_after来补充insert_after

1.2 置入操作占优的条件

在以下条件成立时,置入函数极可能运行的更快:

  • 1,待添加的值是以构造而非赋值方式加入容器;
    • 基于节点的容器几乎总是使用构造来添加新值,以及不基于节点的std::vector, std::dequestd::stringstd::array不基于节点,但也不支持插入和置入)。
    • 在非基于节点的容器中,emplace_back使用构造非赋值降新值就位,std::dequeemplace_front也成立。
  • 2,传递的实参类型与容器持有之物的类型不同;
    • 只有将类型不同的元素插入到容器中时,才会创建和析构临时对象,才能体现置入操作的优点。
  • 3,容器不会由于存在重复值而拒绝待添加的值。
    • 因为如果容器不允许重复值,那么置入操作将会用新值创建一个节点与容器现有节点比较,如果该值已存在,则置入操作就会中止,节点也会被析构,那就意味着构造和析构的成本都被浪费了。

条款1的示例代码如下:

有效率差(创建临时对象):

std::vector<std::string> vs;
//push_back版本
vs.push_back("xyz")
//编译器实际看到的版本
//创建std::string类型临时对象,并将其传递给push_back
vs.push_back(std::string("xyc"))

//emplace_back版本
//直接从xyz出发在vs内构造std::string类型对象
vs.emplace_back("xyz")

效果相同的示例(不需要创建临时对象,也不需要虚构临时对象):

std::vector<std::string> vs;

std::string str("hello")
vs.push_back(str)
vs.emplace_back(str)

以赋值方式加入容器(优势消失):

std::vector<std::string> vs;
vs.emplace(vs.begin(), "xyz");

1.3 注意事项

是否选用置入函数,还有两个问题值得考虑:

  • 1,资源相关的问题(在操作资源管理对象容器时,可能会造成内存泄漏【不应该将new 对象名这样的表达式传递给emplace_backpush_back等大多数函数】);
  • 2,置入函数可能会执行在插入函数中会被拒绝的类型(与带有explicit声明修饰的析构函数之间的互动)【确保传递正确的参数】。

条款1示例代码:

调用push_back:创建std::shared_ptr<Widget>临时对象,用于持有从new Widget返回的裸指针,即使内存不足,插入失败,std::shared_ptr的析构函数也会释放临时对象;
调用emplace_back版本:new Widget返回的裸指针被完美转发,但是如果内存不足,置入失败,指向Widget对象的指针丢失,堆上的Widget对象将造成泄漏。

class Widget{};

void killWidget(Widget* pWidget){}

int main(){
    std::list<std::shared_ptr<Widget>> ptrs;
    //push_back版本
    ptrs.push_back(std::shared_ptr<Widget>(new Widget, killWidget));
    ptrs.push_back({new Widget, killWidget});
    //emplace_back版本
    ptrs.emplace_back(std::shared_ptr<Widget>(new Widget, killWidget));
    ptrs.emplace_back(new Widget, killWidget);
    }

条款一的解决方法(但是emplace_backpush_back版本差别不大):

class Widget{};

void killWidget(Widget* pWidget){}

int main(){
    std::list<std::shared_ptr<Widget>> ptrs;
    std::shared_ptr<Widget> spw(new Widget, killWidget);//构造Widget并用spw管理
    //push_back版本
    //ptrs.push_back(std::move(spw));
    //emplace_back版本
    ptrs.emplace_back(std::move(spw));
    }

条款2示例代码:

调用push_back:编译失败,因为const char*指针类型的std::regex构造函数以explicit声明,类型转换就被阻止了。
调用emplace_back:向std::regex构造函数传递的是个构造函数的实参(并不被视为隐式类型转换),编译器看来等同于std::regex r(nullptr);,这样虽然可以通过编译,但是std::regex接受的却是一个没有意义空指针,问题将会被隐藏。

	//下一行代码无法通过编译,因为复制初始化禁止使用那个构造函数
    std::vector<std::regex> regexs;
    //regexs.push_back(nullptr);//error: no matching member function for call to 'push_back'
    //下面代码正常通过编译
    //直接初始化允许使用接受指着的、带有explicit声明的std::regex构造函数
    regexs.emplace_back(nullptr);

编译结果的详细解释:push_backemplace_back分别对应复制初始化和直接初始化,而复制初始化不允许调用带有explicit声明修饰的构造函数。

//复制初始化
    std::regex r1 = nullptr;//报错
//直接初始化
    std::regex r2(nullptr);