19

看起来像这样的代码

#include <string>
#include <vector>

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

int main()
{
    std::vector<bla> v;
    v.emplace_back("string", 42);
}

在这种情况下可以使其正常工作,但事实并非如此(我明白为什么)。提供bla构造函数可以解决这个问题,但会消除类型的聚合性,这可能会产生深远的影响。

这是标准中的疏忽吗?还是我错过了某些情况下这会在我的脸上爆炸,或者它没有我想的那么有用?

4

2 回答 2

12

这是标准中的疏忽吗?

它被认为是标准中的一个缺陷,跟踪为LWG #2089,由 C++20 解决。在那里,构造函数语法可以对聚合类型执行聚合初始化,只要提供的表达式不会调用复制/移动/默认构造函数。由于所有形式的间接初始化(push_backin_placemake_*等)都显式使用构造函数语法,因此它们现在可以初始化聚合。

在 C++20 之前,一个很好的解决方案是难以捉摸的。

根本问题来自这样一个事实,即您不能随便使用braced-init-lists。使用构造函数的类型的列表初始化实际上可以隐藏构造函数,使得某些构造函数无法通过列表初始化调用。这就是vector<int> v{1, 2};问题所在。这将创建一个 2-element vector,而不是唯一元素为 2 的 1-element 向量。

因此,您不能在通用上下文中使用列表初始化,例如allocator::construct.

这使我们:

如果可能的话,我认为有一个 SFINAE 技巧可以做到这一点,否则请使用同样适用于聚合的大括号 init。

那将需要一个is_aggregate类型特征。目前不存在,也没有人提出它的存在。哦,当然,您可以使用is_constructible,正如对该问题提出的解决方案所述。但是这样做有一个问题:它有效地创建了列表初始化的替代方案。

考虑vector<int>之前的那个例子。{1, 2}被解释为二元initializer_list。但是通过emplace,它将被解释为调用两个整数构造函数,因为is_constructible从这两个元素中将是正确的。这导致了这个问题:

vector<vector<float>> fvec;
fvec.emplace(1.0f, 2.0f);
vector<vector<int>> ivec;
ivec.emplace(1, 2);

它们做了两件完全不同的事情。在这种fvec情况下,它执行列表初始化,因为vector<float>不能从两个浮点数构造。在这种ivec情况下,它调用构造函数,因为vector<int>可以从两个整数构造。

因此,您需要将列表初始化限制为仅在聚合allocator::construct时才起作用。T

即使你这样做了,你也必须将这个 SFINAE 技巧传播到所有使用间接初始化的地方。这包括any/variant/optionalin_place构造函数和位置、make_shared/unique调用等,其中没有一个使用allocator::construct.

这还不包括需要这种间接初始化的用户代码。如果用户没有像 C++ 标准库那样做同样的初始化,人们会很不高兴。

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

于 2017-10-31T15:42:01.603 回答
5

23.2.1/15.5

T is EmplaceConstructible into X from args,对于零个或多个参数 args,意味着以下表达式是格式良好的:

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),但专门的分配器可能会选择不同的定义。——尾注]

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

还有一个问题“走向更完美的转发”和一些关于它的未来的随机讨论。

于 2017-04-14T09:12:14.673 回答