C++11中使用结构体的emplace_back和push_back语法

23

我将使用 MSVC,Visual Studio 2013。

假设我有一个结构体:

struct my_pair {
    int foo, bar;
};

我希望能高效地添加一堆这些内容,而不需要创建一个临时变量再把它丢掉:

vector<my_pair> v;
v.push_back(41, 42); // does not work              [a]
v.push_back({41,42}); // works                     [b]
v.emplace_back(41,42); // does not work            [c]
v.emplace_back({41,42}); // does not work          [d]
v.emplace_back(my_pair{41,42}); //works            [e]

如果我在我的代码中添加构造函数和复制构造函数:

my_pair(int foo_, int bar_) : foo(foo_), bar(bar_) 
{
    cout << "in cstor" << endl;
}
my_pair(const my_pair& copy) : foo(copy.foo), bar(copy.bar)
{
    cout << "in copy cstor" << endl;
}

然后行为发生了改变:

v.push_back(41, 42); // does not work                              [f]
v.push_back({41,42}); // displays "in cstor" and "in copy cstor"   [g]
v.emplace_back(41,42); // displays "in cstor"                      [h]
v.emplace_back({41,42}); // does not work                          [i]
v.emplace_back(my_pair{41,42}); // "in cstor" and "in copy cstor"  [j]

如果我添加了一个移动构造函数:
my_pair(my_pair&& move_) : foo(move_.foo), bar(move_.bar)
{
    cout << "in move cstor" << endl;
}

那么:

v.emplace_back(my_pair{41,42}); //displays "in cstor", "in move cstor"   [k]
v.emplace_back({41,42}); // still does not work                          [l]
v.push_back({41,42}); // displays "in cstor", "in move cstor"            [m]
问题:
对于[a,b],我理解为原因是有效和无效的。
对于[c],它不起作用是因为没有构造函数将参数转发到其中。
对于[d],为什么它不能像push case一样工作?
对于[e],为什么在添加类名时它可以工作?
对于[h],如果有一个将参数映射到成员的构造函数,它似乎是最有效的代码。
对于[j],这似乎与push_back一样糟糕,而且额外的打字让我不确定为什么任何人都应该在push_back上使用这个函数。
对于[k,m],增加了移动构造函数后,看起来正在调用 push_back(T&&) ,其结果与emplace相同。但是,再次出现额外的打字,我不确定为什么任何人都会这样做。

我读到MSVC不会自动添加移动构造函数:Why is copy constructor called in call to std::vector::emplace_back()?

[d,e]之间有什么区别,为什么emplace很挑剔。为什么 push_back(T&&) 可以工作而不需要添加结构体名称?

我只有在知道有一个构造函数以每个成员作为参数时才能充分利用emplace的好处吗?

我应该坚持使用 push_back 吗?有没有理由使用 emplace_back(structname {1,2,3})而不是使用 push_back({1,2,3}),因为它最终会调用 push_back(T&&),并且打字更容易?

第三,emplace_back(arg1,arg2,etc)如何进行魔法以完全避免复制或移动构造函数?


作为一个快速评论,emplace_back({42, 42}通常不起作用,因为它变成了两个整数的initializer-list,而不是一个my_pair结构体。我认为如果你还添加了一个initializer list构造函数,这个emplace_back调用可能会起作用。当然,人们可能会认为这会直接起作用,但你还必须考虑到emplace_back的完美转发语义: 在大多数情况下,除非你明确命名类型,否则像emplace_back这样的完美转发函数将以所给定的一切为准,没有额外的考虑或转换。 - user3010322
1个回答

15

对于 v.emplace_back({41,42});,请参见 如何使用 std::vector::emplace_back 来处理 vector<vector<int> >?


v.emplace_back(41,42);由于标准中的某些规则,无法工作(一些强调是我的):

表101 — 可选序列容器操作

表达式:a.emplace_back(args)

返回类型:void

操作语义:
追加一个使用std::forward<Args>(args)...构造的类型为T的对象。

要求:T必须可以从argsEmplaceConstructibleX。对于vector,T还应该可以MoveInsertable到X。

要使类型成为EmplaceConstructible

§ 23.2.1.13

—— 当零个或多个参数args使以下表达式成立时,T可以从args中的内容进行就地构造(EmplaceConstructible)到X中:

allocator_traits<A>::construct(m, p, args);

std::allocator_traits::construct()反过来会执行a.construct(p, std::forward<Args>(args)...)(其中aEmplaceConstructible表达式中的m)。

a.construct()在此处指std::allocator::construct(),它调用::new((void *)p) U(std::forward<Args>(args)...)这是导致编译错误的原因。

U(std::forward<Args>(args)...)(注意使用直接初始化)将查找接受转发参数的U构造函数。然而,在您的情况下,my_pair是一种聚合类型,只能使用大括号初始化语法(聚合初始化)进行初始化。


v.emplace_back(my_pair{41,42});的工作原理是调用隐式生成的默认复制构造函数或移动构造函数(请注意,这两者可能不总是生成)。首先构造一个临时的my_pair,它经历了与v.emplace_back(41,42);相同的过程,只是参数是一个r-value my_pair


附加说明1:

为什么 push_back(T&&) 不需要添加结构体名称就可以工作?

这是因为 push_back的签名push_back()的参数不是推导出来的,这意味着通过执行push_back({1, 2}),首先创建并初始化一个临时对象,其类型为向量元素类型的类型为{1, 2}。然后将传递给push_back(T&&)的对象是该临时对象。


我应该坚持使用push_back吗?使用emplace_back(structname{1,2,3})而不是push_back({1,2,3})有什么理由,因为它最终会调用push_back(T&&),而且更容易输入?
基本上,emplace*函数旨在优化并消除在插入对象时创建临时对象和复制或移动构造对象的成本。但是,在聚合数据类型的情况下,像emplace_back(1, 2, 3)这样的操作是不可能的,您唯一能够插入它们的方式是通过创建临时对象然后复制或移动,那么请优先选择更简洁的语法,使用push_back({1,2,3}),其中它的性能基本上与emplace_back(structname{1,2,3})相同。

2
我不同意你在答案最后一段中关于“无法插入聚合体而不复制”的说法。你总是可以这样做:v.emplace_back(); v.back().foo = 41; v.back().bar = 42; - PowerGamer
1
@PowerGamer,也就是说,如果聚合体可以默认构造的话。但是没错,这是你需要添加的一个很好的观点。(直到你指出来我才真正意识到它,感谢你) - Mark Garcia

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