C++11 中的 emplace 存在的意义与注意事项

5,151 阅读1分钟

「这是我参与11月更文挑战的第 10 天,活动详情查看:2021最后一次更文挑战」。

参加该活动的第 21 篇文章

对比

大部分的容器在 C++11 中对于添加元素除了常见的 insert 或者 pusb_back/push_front 之外还提供一个新的方法叫做 emplace。 比如你想要向 std::vector 的末尾添加一个元素,示例代码如下:

/// @note 传统方式
std::vector<int> nums;
nums.push_back(1);

/// @note C++11
std::vector<int> nums;
nums.emplace_back(1);

优点

使用 emplace 的优点是避免产生不必要的临时变量,避免不必要的临时对象的产生,举个例子:

struct Foo {
    Foo(int n, double x);
};

std::vector<Foo> v;
v.emplace(someIterator, 42, 3.1416);     ///< 没有临时变量产生
v.insert(someIterator, Foo(42, 3.1416)); ///< 需要产生一个临时变量
v.insert(someIterator, {42, 3.1416});    ///< 需要产生一个临时变量

原理

从以上的例子中,我们可以看出 emplace 相较于 insertemplace 的语法看起来比较特别,后面两个参数自动用来构造 vector 内部的 Foo 对象。这是因为其内部利用了 C++11 的两个新特性 —— 变参模板完美转发

  • [变参模板] 使得 emplace 可以接受任意参数,这样就可以适用于任意对象的构建。
  • [完美转发] 使得接收下来的参数能够原样完美地传递给对象的构造函数,这带来另一个方便性就是即使是构造函数声明为 explicit 它还是可以正常工作,因为它不存在临时变量和隐式转换。
struct Bar
{
    Bar(int a) {}
    explicit Bar(int a, double b) {} ///< 必须显示调用
};

int main(void)
{
    vector<Bar> bv;
    bv.push_back(1);      ///< 隐式转换生成临时变量
    bv.push_back(Bar(1)); ///< 显示构造临时变量
    bv.emplace_back(1);   ///< 没有临时变量

    //bv.push_back({1, 2.0}); ///< 无法进行隐式转换
    bv.push_back(Bar(1, 2.0));///< 显示构造临时变量
    bv.emplace_back(1, 2.0);  ///< 没有临时变量

    return 0;
}

注意

值得注意的是 map 类型的 emplace 处理比较特殊,因为和其他的容器不同,mapemplace 方法把它接收到的所有的参数都一起转发给 pair 的构造函数。但是对于一个 pair 来说,它既需要构造它的 key 又需要构造它的 value。如果我们按照之前普通的语法使用变参模板的话,则它是无法区分哪些参数用来构造 key, 哪些用来构造 value的。 比如下面的代码

// @note 无法区分哪个参数用来构造 key 哪些用来构造 value
// 有可能是 string s("hello", 1), complex<double> cpx(2) 
// 也有可能是 string s("hello"), complex<double> cpx(1, 2)
map<string, complex<double>> scp;
scp.emplace("hello", 1, 2);