跳转至

Tip of the Week #65: Putting Things in their Place

Originally posted as totw/65 on 2013-12-12 By Hyrum Wright (hyrum@hyrumwright.org) “Let me ’splain. No, there is too much. Let me sum up.” –Inigo Montoya

C++11中添加了一种新方式往标准容器中插入元素,那就是emplace()系列方法了。这些方法会直接在容器中创建对象,避免创建临时对象,然后通过拷贝或者移动对象到容器中。这对于几乎所有的对象来说避免了拷贝,更加高效。尤其是对于往标准容器中存储只能移动的对象(例如std::unique_ptr)就更为方便了。

The Old Way and the New Way

让我们通过使用vector来存储的一个简单的例子来对比下两种方式。第一个例子是C++11之前的编码风格:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Foo {
 public:
  Foo(int x, int y);
  
};

void addFoo() {
  std::vector<Foo> v1;
  v1.push_back(Foo(1, 2));
}
通过使用老的push_back方法,会导致Foo对象被构造两次,一次是临时构造一个Foo对象,然后将临时对象进行移动构造,放到容器中。

我们可以使用C++11引入的emplace_back(),这种方式只会引入一个对象的构造,是直接在vector容器元素所在内存上构造。正是由于emplace系列函数将其参数直接转发给底层对象的构造函数,因此我们可以直接提供构造函数参数,从而无需创建临时的Foo对象。

1
2
3
4
void addBetterFoo() {
  std::vector<Foo> v2;
  v2.emplace_back(1, 2);
}

Using Emplace Methods for Move-Only Operations

到目前为止,我们已经研究过emplace方法可以提高性能的情况,此外它可以让之前不能工作的代码可以正常工作,例如容器中的类型是只能被移动的类型像std::unique_ptr。考虑下面这段代码:

1
std::vector<std::unique_ptr<Foo>> v1;
如何才能向这个容器中插入一个元素呢? 一种方式就是通过push_back直接在参数中构造对象:

1
v1.push_back(std::unique_ptr<Foo>(new Foo(1, 2)));
这种语法有效,但可能有点笨拙。不幸的是,解决这种混乱的传统方式充满了复杂性:

1
2
Foo *f2 = new Foo(1, 2);
v1.push_back(std::unique_ptr<Foo>(f2));
上面这段代码可以编译,但是它使得在被插入前,指针的所有权变的不清晰。甚至更糟糕的是,vector拥有了该对象,但是f2仍然有效,并且有可能在此后被删除。对于不知情的读者来说,这种所有权模式可能会令人困惑,特别是如果构造和插入不是如上所述的顺序事件。

其他的解决方案甚至都无法编译,因为unique_ptr是不能被拷贝的:

1
2
3
std::unique_ptr<Foo> f(new Foo(1, 2));
v1.push_back(f);             // Does not compile!
v1.push_back(new Foo(1, 2)); // Does not compile!
使用emplace方法会使得对象的创建更为直观,如果你需要把unique_ptr放到vector容器中,可以像下面这样通过std::move来完成:

1
2
3
std::unique_ptr<Foo> f(new Foo(1, 2));
v1.emplace_back(new Foo(1, 2));
v1.push_back(std::move(f));

通过把emplace和标准的迭代器结合,可以将对象插入到vector容器中的任何位置:

1
v1.emplace(v1.begin(), new Foo(1, 2));
实际上,我们不希望看到上述构造unique_ptr的这些方法,而是希望通过std::make_unique(C++14),或者是absl::make_unique(C++11)。

Conclusion

本文使用vector来作为example中的标准容器,实际上emplace同样也适用于maplist以及其它的STL容器。当unique_ptremplace结合,使得在堆上分配的对象其所有权的语义更加清晰。希望通过本文能让您感受到新的容器方法的强大功能,以及满足在您自己的代码中适当使用它们的愿望。