push_back比emplace_back更高效吗?

6
我想看一下push_back和emplace_back之间的区别,因为在很多地方我读到了推荐使用emplace_back的建议,因为“它可以做所有push_back能做的事情,而且更多”,所以我期望它更高效。但令我惊讶的是
#include <iostream>     
#include <vector>    

class A
{
public:
    A() { std::cout << "A const\n"; }
    ~A() { std::cout << "A dest\n"; }
    A(const A& a) { std::cout << "A copy const\n"; }
    A(A&& a) { std::cout << "A move const\n"; }
    A& operator=(const A& a) { std::cout << "A copy operator=\n"; return *this; }
    A& operator=(A&& a) { std::cout << "A move operator=\n";  return *this; }
};

int main () {
    std::vector<A> va;

    std::cout << "push:\n" << std::endl;
    va.push_back(A());

    std::cout << "\nemplace:\n";
    va.emplace_back(A());
    
    std::cout << "\nend:\n";

    return 0;
}

输出是

push:
A const
A move const
A dest

emplace:
A const
A move const
A copy const
A dest
A dest

end:
A dest
A dest

emplace_back调用移动构造函数,然后在push_back调用一个移动构造函数时复制一个。我已经使用g++(Ubuntu 7.4.0-1ubuntu1~16.04~ppa1)7.4.0和在线C++ shell进行了检查。 我有什么遗漏吗?

2
现在按照另一种顺序做,会发生什么?(同时不要使用“const”作为构造函数的缩写...“ctor”更好,因为它不会与“const”冲突)。 - Barry
2
当您插入第二个元素时,向量将重新分配其存储空间(因此必须将第一个元素复制到新的存储空间)。先使用emplace_back并比较结果。 - HolyBlackCat
1
  1. 移动构造函数不是noexcept,因此vector调用复制。
  2. 您重复使用同一向量并导致重新分配。
  3. 您使用临时对象调用emplace_back,这样做会失去目的。
- Sopel
2
还有另一种你可以尝试来完成这个实验,那就是emplace_back() - 注意没有参数。 - StoryTeller - Unslander Monica
3
现在,只需调用va.emplace_back();即可,无需构造一个完全无用的临时对象来移动构造向量中的值。 - Sam Varshavchik
非常感谢大家的评论! - DoehJohn
1个回答

14

push_back 没有更高效,你观察到的结果是由于 vector 调整大小本身所致。

当你在 push_back 之后调用 emplace,向量必须重新调整大小以为第二个元素腾出空间。这意味着它必须移动最初在向量内部的 A,使得 emplace 看起来更加复杂。

如果你事先保留了向量中足够的空间,这种情况就不会发生。请注意,在创建 va 之后调用 va.reserve(2)

#include <iostream>     
#include <vector>    

class A
{
    public:
    A() {std::cout << "A const" << std::endl;}
    ~A() {std::cout << "A dest" << std::endl;}
    A(const A& a) {std::cout << "A copy const" << std::endl;}
    A(A&& a) {std::cout << "A move const" << std::endl;}
    A& operator=(const A& a) {std::cout << "A copy operator=" << std::endl; return *this; }
    A& operator=(A&& a) {std::cout << "A move operator=" << std::endl;  return *this; }
};

int main () {
    std::vector<A> va;
    // Now there's enough room for two elements
    va.reserve(2);
    std::cout <<"push:" << std::endl;
    va.push_back(A());
    std::cout <<std::endl<< "emplace:" << std::endl;
    va.emplace_back(A());

    std::cout <<std::endl<< "end:" << std::endl;

    return 0;
}

相应的输出为:

push:
A const
A move const
A dest

emplace:
A const
A move const
A dest

end:
A dest
A dest

我们能让事情变得更加高效吗? 可以!emplace_back 接受您提供的任何参数,并将它们转发给 A 的构造函数。 因为 A 有一个不带参数的构造函数,所以您也可以使用没有参数的 emplace_back!换句话说,我们改为:

va.emplace_back(A());

va.emplace_back(); // No arguments necessary since A is default-constructed

这将不会进行复制或移动:

push:
A const
A move const
A dest

emplace:
A const

end:
A dest
A dest

关于向量调整大小的说明:需要注意的是,std::vector 的实现非常聪明。如果A是一个可以简单复制的类型,std::vector可能会使用类似于realloc的系统函数就地调整大小,而无需进行其他复制。但是,由于A的构造函数和析构函数包含代码,因此在这里不能使用realloc


3
请提供需要翻译的完整上下文,以便更好地进行翻译。 - Sopel
非常感谢!除了这个方面之外,当我将对象传递给emplace_back时,它与push_back相同,对吗?A a,a1; va.emplace_back(a); va.push_back(a1); 都会调用复制构造函数。虽然我认为emplace_back默认应该进行移动。 - DoehJohn
2
当您插入第一个元素时,std::vector会为超过2个元素分配空间。性能下降是由于OP使用emplace_back()的方式。emplace_back()将构造函数的参数作为参数输入。使用默认构造对象作为参数调用emplace_back会调用移动构造函数。因此,emplace_back(A{})是1个构造和1个移动。调用push_back(A{})是1个构造和1个复制。调用v.resize(v.size()+1)是1个原地构造,速度最快。对于像示例中的简单类,复制可能比移动更快,这是一种交换。 - Michaël Roy
无论是 push_back 还是 emplace_back,当提供一个类型为 A&& 的对象时,它们都会移动该对象。移动对象的问题在于它并不会阻止析构函数的调用,因为仍然可能需要进行清理。有一些提案允许程序员指示对象是可以被平凡移动的,以避免不必要的析构函数调用,但这些提案尚未被纳入标准。 - Alecto Irene Perez
在移动操作中,对象被交换并且析构函数仍然被调用。在这个例子中,析构函数是微不足道的。 - Michaël Roy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接