为什么 emplace_back() 不使用统一初始化?

2021-12-21 00:00:00 vector c++ c++11 uniform-initialization

以下代码:

#include <vector>

struct S
{
    int x, y;
};

int main()
{
    std::vector<S> v;
    v.emplace_back(0, 0);
}

使用 GCC 编译时出现以下错误:

Gives the following errors when compiled with GCC:

In file included from c++/4.7.0/i686-pc-linux-gnu/bits/c++allocator.h:34:0,
                 from c++/4.7.0/bits/allocator.h:48,
                 from c++/4.7.0/vector:62,
                 from test.cpp:1:
c++/4.7.0/ext/new_allocator.h: In instantiation of 'void __gnu_cxx::new_allocator<_Tp>::construct(_Up*, _Args&& ...) [with _Up = S; _Args = {int, int}; _Tp = S]':
c++/4.7.0/bits/alloc_traits.h:265:4:   required from 'static typename std::enable_if<std::allocator_traits<_Alloc>::__construct_helper<_Tp, _Args>::value, void>::type std::allocator_traits<_Alloc>::_S_construct(_Alloc&, _Tp*, _Args&& ...) [with _Tp = S; _Args = {int, int}; _Alloc = std::allocator<S>; typename std::enable_if<std::allocator_traits<_Alloc>::__construct_helper<_Tp, _Args>::value, void>::type = void]'
c++/4.7.0/bits/alloc_traits.h:402:4:   required from 'static void std::allocator_traits<_Alloc>::construct(_Alloc&, _Tp*, _Args&& ...) [with _Tp = S; _Args = {int, int}; _Alloc = std::allocator<S>]'
c++/4.7.0/bits/vector.tcc:97:6:   required from 'void std::vector<_Tp, _Alloc>::emplace_back(_Args&& ...) [with _Args = {int, int}; _Tp = S; _Alloc = std::allocator<S>]'
test.cpp:11:24:   required from here
c++/4.7.0/ext/new_allocator.h:110:4: error: new initializer expression list treated as compound expression [-fpermissive]
c++/4.7.0/ext/new_allocator.h:110:4: error: no matching function for call to 'S::S(int)'
c++/4.7.0/ext/new_allocator.h:110:4: note: candidates are:
test.cpp:3:8: note: S::S()
test.cpp:3:8: note:   candidate expects 0 arguments, 1 provided
test.cpp:3:8: note: constexpr S::S(const S&)
test.cpp:3:8: note:   no known conversion for argument 1 from 'int' to 'const S&'
test.cpp:3:8: note: constexpr S::S(S&&)
test.cpp:3:8: note:   no known conversion for argument 1 from 'int' to 'S&&'

建议 vector 使用常规的 () 构造函数语法从 emplace_back() 的参数构造元素.为什么 vector 不使用 {} 统一初始化语法来代替上述示例?

Suggesting that vector is using regular () constructor syntax to construct the element from the arguments to emplace_back(). Why doesn't vector use the {} uniform-initialization syntax instead, to make examples like the above work?

在我看来,使用 {} 没有什么可失去的(它在有一个时调用构造函数,但在没有时仍然有效),而且它会更多本着 C++11 的精神使用 {} - 毕竟,uniform 初始化的全部意义在于它统一使用 - 即无处不在 - 来初始化对象.

It seems to me that there is nothing to lose by using {} (it calls the constructor when there is one, but still works when there isn't one), and it would be more in the spirit of C++11 to use {} - after all, the whole point of uniform initialization is that it is used uniformly - that is, everywhere - to initialize objects.

推荐答案

伟大的思想都是一样的 ;v) .我提交了一份缺陷报告,并建议更改有关此主题的标准.

Great minds think alike ;v) . I submitted a defect report and suggested a change to the standard on this very topic.

http://cplusplus.github.com/LWG/lwg-active.html#2089

此外,Luc Danton 帮助我理解了困难:std 中的直接与统一初始化::分配器.

Also, Luc Danton helped me understand the difficulty: Direct vs uniform initialization in std::allocator.

当 EmplaceConstructible (23.2.1[container.requirements.general]/13)requirements 用于初始化一个对象,直接初始化发生.初始化聚合或使用带有 emplace 的 std::initializer_list 构造函数需要命名初始化类型并移动一个临时的.这是一个结果std::allocator::construct 使用直接初始化,而不是列表初始化(有时称为统一初始化")语法.

When the EmplaceConstructible (23.2.1 [container.requirements.general]/13) requirement is used to initialize an object, direct-initialization occurs. Initializing an aggregate or using a std::initializer_list constructor with emplace requires naming the initialized type and moving a temporary. This is a result of std::allocator::construct using direct-initialization, not list-initialization (sometimes called "uniform initialization") syntax.

改变 std::allocator::construct 以使用列表初始化除其他外,会优先考虑 std::initializer_list构造函数重载,以不直观的方式破坏有效代码无法修复的方式――emplace_back 无法访问构造函数被 std::initializer_list 抢占,本质上没有重新实现 push_back.

Altering std::allocator::construct to use list-initialization would, among other things, give preference to std::initializer_list constructor overloads, breaking valid code in an unintuitive and unfixable way ― there would be no way for emplace_back to access a constructor preempted by std::initializer_list without essentially reimplementing push_back.

std::vector<std::vector<int>> v;
v.emplace_back(3, 4); // v[0] == {4, 4, 4}, not {3, 4} as in list-initialization

提议的折衷方案是将 SFINAE 与 std::is_constructible 一起使用,它测试直接初始化是否格式良好.如果is_constructible 是假的,那么另一种选择选择 std::allocator::construct 重载使用列表初始化.由于列表初始化总是依赖于直接初始化,用户将看到诊断消息,好像列表初始化(统一初始化)总是被使用,因为直接初始化重载不会失败.

The proposed compromise is to use SFINAE with std::is_constructible, which tests whether direct-initialization is well formed. If is_constructible is false, then an alternative std::allocator::construct overload is chosen which uses list-initialization. Since list-initialization always falls back on direct-initialization, the user will see diagnostic messages as if list-initialization (uniform-initialization) were always being used, because the direct-initialization overload cannot fail.

我可以看到两个极端案例暴露了这个方案中的漏洞.一发生当用于 std::initializer_list 的参数满足 a构造函数,例如尝试在上面的例子.解决方法是明确指定std::initializer_list 类型,如v.emplace_back(std::initializer_list(3, 4)).由于这匹配语义就好像推导了 std::initializer_list 一样,似乎有在这里没问题.

I can see two corner cases that expose gaps in this scheme. One occurs when arguments intended for std::initializer_list satisfy a constructor, such as trying to emplace-insert a value of {3, 4} in the above example. The workaround is to explicitly specify the std::initializer_list type, as in v.emplace_back(std::initializer_list(3, 4)). Since this matches the semantics as if std::initializer_list were deduced, there seems to be no real problem here.

另一种情况是用于聚合初始化的参数满足一个构造函数.由于聚合不能具有用户定义的构造函数,这要求第一个非静态数据成员聚合可以从聚合类型隐式转换,并且初始化列表有一个元素.解决方法是为第二个成员提供初始值设定项.仍然不可能就地构造一个只有一个非静态数据成员的聚合从可转换类型到聚合自身类型的转换.这似乎是一个可以接受的小洞.

The other case is when arguments intended for aggregate initialization satisfy a constructor. Since aggregates cannot have user-defined constructors, this requires that the first nonstatic data member of the aggregate be implicitly convertible from the aggregate type, and that the initializer list have one element. The workaround is to supply an initializer for the second member. It remains impossible to in-place construct an aggregate with only one nonstatic data member by conversion from a type convertible to the aggregate's own type. This seems like an acceptably small hole.

相关文章