【问题标题】:Why can an aggreggate struct be brace-initialized, but not emplaced using the same list of arguments as in the brace initialization?为什么聚合结构可以进行大括号初始化,但不能使用与大括号初始化中相同的参数列表来放置?
【发布时间】:2017-09-10 11:50:59
【问题描述】:

好像this code:

#include <string>
#include <vector>

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

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

在这种情况下可以正常工作,但它不能(我明白为什么)。给 bla 一个构造函数可以解决这个问题,但会消除类型的聚合性,这可能会产生深远的影响。

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

【问题讨论】:

  • 长话短说:emplace_back(以及所有类似的转发工厂函数,例如std::make_unique)总是使用括号来构造对象。为什么会这样,我不知道。
  • @Quentin 这与参数的完美转发有关。如果可能的话,我认为有一个 SFINAE 技巧可以做到这一点,否则请使用同样适用于聚合的大括号 init。

标签: c++ c++11 aggregate emplace


【解决方案1】:

这是标准中的疏忽吗?

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

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

根本问题在于你不能随便使用braced-init-lists。带有构造函数的类型的列表初始化实际上可以隐藏构造函数,这样某些构造函数可能无法通过列表初始化来调用。这是vector&lt;int&gt; v{1, 2}; 问题。这将创建一个 2 元素 vector,而不是唯一元素为 2 的 1 元素向量。

因此,您不能在像allocator::construct 这样的通用上下文中使用列表初始化。

这让我们:

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

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

考虑之前的vector&lt;int&gt; 示例。 {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&lt;float&gt; 不能从两个浮点数构造。在ivec 的情况下,它调用构造函数,因为vector&lt;int&gt; 可以从两个整数构造。

所以您需要allocator::construct 中的列表初始化限制为仅在T 是聚合时才起作用。

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

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

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

【讨论】:

    【解决方案2】:

    23.2.1/15.5

    对于零个或多个参数,T 是 EmplaceConstructible 到 X 从 args args, 表示下面的表达式是良构的:

    allocator_traits&lt;A&gt;::construct(m, p, args)

    23.2.1/15

    [注意:容器调用allocator_traits&lt;A&gt;::construct(m, p, args) 使用args 在p 处构造一个元素。 std::allocator 中的默认构造将调用 ::new((void*)p) T(args),但专门的分配器可能会选择不同的定义。 ——尾注]

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

    还有一个关于它的未来的问题"Towards more perfect forwarding" 和一些随机的discussion

    【讨论】:

    • 我看不到您如何通过阅读链接的答案最终得出“更改此行为可能导致向后兼容性损失”的结果。你能详细说明一下吗?
    • @rubenv 我从原始源 (cplusplus.github.io/LWG/lwg-active.html#2089) 获取它“更改 std::allocator&lt;T&gt;::construct 以使用列表初始化,除其他外,优先考虑 std::initializer_list 构造函数重载,破坏有效代码一种不直观且无法修复的方式……”。
    • 这似乎是一个不合格的陈述。当且仅当非列表初始化调用不可能时,它才可以使用列表初始化。嗯,这不是我想讨论的地方。
    • 似乎建议的解决方案实际上就是这样做的。在宏伟的计划中,这只是次优。
    猜你喜欢
    • 2013-08-23
    • 2012-11-21
    • 2023-03-11
    • 2016-12-11
    • 1970-01-01
    • 1970-01-01
    • 2016-04-14
    • 2020-06-02
    • 2021-12-31
    相关资源
    最近更新 更多