为什么聚合结构可以使用大括号初始化,但不能使用与大括号初始化中相同的参数列表进行嵌入式初始化?

21

看起来像是这段代码

#include <string>
#include <vector>

struct bla
{
    std::string a;
    int b;
};

int main()
{
    std::vector<bla> v;
    v.emplace_back("string", 42);
}
在这种情况下可能可以使其正常工作,但它没有(我理解原因)。给bla一个构造函数可以解决这个问题,但会移除类型的聚合性,这可能会产生深远的影响。
这在标准中是一个疏忽吗?还是我没有考虑到某些情况,这可能会让我掉进坑里,或者它并不像我想象的那么有用?

1
长话短说:emplace_back(以及所有类似的转发工厂函数,如std::make_unique)始终使用括号来构造对象。为什么会这样,我不知道。 - Quentin
1
@Quentin 这与参数的完美转发有关。如果可能的话,我认为会有一个SFINAE技巧来实现这一点,否则可以使用花括号初始化,这也适用于聚合体。 - rubenvb
在聚合结构/类上,创建对象的中间副本并将其插入向量之前通常不会太糟糕,因为大多数操作可以内联。因此,v.push_back(bla { "string", 42 }); 可用于从C++11到C++17的C++标准。从C++20开始,v.emplace_back("string", 42); 就能正常工作了。 - Kai Petzke
2个回答

13
这是标准中的疏忽吗?
这被认为是标准中的缺陷,追踪编号为LWG#2089,已由C ++ 20解决。在那里,构造函数语法可以对聚合类型执行聚合初始化,只要提供的表达式不会调用复制/移动/默认构造函数。由于所有形式的间接初始化(push_backin_placemake_*等)都明确使用构造函数语法,因此它们现在可以初始化聚合体。
在C++20之前,很难找到一个好的解决方案。
根本问题在于您不能随意使用大括号初始化列表。具有构造函数的类型的列表初始化实际上可以隐藏构造函数,从而某些构造函数可能无法通过列表初始化调用。这就是vector<int> v {1,2};问题。这将创建一个2个元素的vector,而不是仅包含2的1个元素的vector
因此,在像allocator::construct这样的通用环境中,您不能使用列表初始化。
这就带来了:
我认为如果可能的话,应该有SFINAE技巧来解决这个问题,否则就退而求其次,使用也适用于聚合体的大括号初始化。
那需要使用C++17的is_aggregate类型特征。但这里有个问题:你需要将这个SFINAE技巧传播到所有使用间接初始化的地方,包括any/variant/optionalin_place构造函数和插入操作、make_shared/unique调用等,这些都没有使用allocator::construct

而且这还不包括用户代码中需要进行间接初始化的情况。如果用户没有像C++标准库一样进行相同的初始化,人们会感到不满。

这是一个棘手的问题,解决办法不会将间接初始化API分为允许聚合体和不允许聚合体的组。有许多可能的解决方案,但没有一种是理想的。

语言解决方案是最好的解决办法。


5

23.2.1/15.5

如果 T 可以使用 args 在 X 中进行 EmplaceConstructible 的话,这意味着以下表达式是良构的:

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

23.2.1/15

[注:容器会调用 allocator_traits<A>::construct(m, p, args) 使用 args 在 p 上构造一个元素。默认情况下,std::allocator 中的默认构造函数将调用 ::new((void*)p) T(args),但专门的分配器可能会选择不同的定义。—end note]

因此,默认分配器使用构造函数,更改此行为可能会导致向后兼容性丢失。您可以在此答案中阅读更多信息:https://dev59.com/RWoy5IYBdhLWcg3wFqJT#8783004

还有一个问题"Towards more perfect forwarding",以及一些关于其未来的随机讨论


我不明白从阅读链接的答案中如何得出“更改此行为可能会导致向后兼容性丢失”的结论。您能详细说明一下吗? - rubenvb
@rubenv 我从原始来源(http://cplusplus.github.io/LWG/lwg-active.html#2089)中获取了这个信息:"将`std::allocator<T>::construct更改为使用列表初始化将会在其他方面优先考虑std::initializer_list`构造函数重载,以一种不直观且无法修复的方式破坏有效代码..."。 - DAle
这似乎是一个不太合格的陈述。只有在非列表初始化调用不可能时,它才能使用列表初始化。嗯,我想这不是讨论这个问题的地方。 - rubenvb
它似乎确实是所建议的解决方案。但从更大的方面来看,这只是次优的。 - rubenvb

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