【问题标题】:Why does aggregate initialization not work anymore since C++20 if a constructor is explicitly defaulted or deleted?如果构造函数被显式默认或删除,为什么自 C++20 以来聚合初始化不再起作用?
【发布时间】:2019-12-07 19:55:48
【问题描述】:

我正在将 C++ Visual Studio 项目从 VS2017 迁移到 VS2019。

我现在遇到了一个以前没有发生过的错误,可以用这几行代码重现:

struct Foo
{
    Foo() = default;
    int bar;
};
auto test = Foo { 0 };

错误是

(6):错误 C2440:“正在初始化”:无法从 'initializer list' to 'Foo'

(6): 注意:没有构造函数可以采用源类型,或者 构造函数重载决议不明确

项目使用/std:c++latest 标志编译。我在godbolt 上复制了它。如果我将它切换到/std:c++17,它会像以前一样编译。

我尝试用clang-std=c++2a 编译相同的代码并得到类似的错误。此外,默认或删除其他构造函数也会产生此错误。

显然,VS2019 中添加了一些新的 C++20 功能,我假设此问题的根源在 https://en.cppreference.com/w/cpp/language/aggregate_initialization 中进行了描述。 在那里它说聚合可以是一个结构(在其他标准中)具有

  • 没有用户提供的、继承的或显式的构造函数(允许显式默认或删除的构造函数)(C++17 起)(C++20 前)
  • 没有用户声明或继承的构造函数(C++20 起)

请注意,括号中的“允许显式默认或删除的构造函数”部分已被删除,并且“用户提供”更改为“用户声明”。

所以我的第一个问题是,我是否正确假设标准的这种变化是我的代码以前编译但现在不再编译的原因?

当然,解决这个问题很容易:只需删除显式默认的构造函数即可。

但是,我在所有项目中都显式默认并删除了很多构造函数,因为我发现以这种方式使代码更具表现力是一个好习惯,因为与隐式默认或删除构造函数相比,它只会产生更少的意外。然而,随着这种变化,这似乎不再是一个好习惯了......

所以我的实际问题是: 从 C++17 到 C++20 的这种变化背后的原因是什么?这种向后兼容性的突破是故意的吗?是否有一些权衡,比如“好吧,我们在这里打破了向后兼容性,但这是为了更大的利益。”?这更大的好处是什么?

【问题讨论】:

  • This is the paper。我不认为理由(IMO 归结为“天啊,这些人为的例子太令人惊讶了,它们必须修复”)具有说服力。 YMMV。
  • 我仍然认为这是一个好习惯,我宁愿认为聚合初始化是一个坏习惯......
  • 为了回答不太重要的第一个问题,显式默认的构造函数被认为是用户声明的,但不是用户提供的。因此,措辞的变化确实是出现新错误的原因。 (作为说明,标准在“用户声明”一词方面有些动摇,因为没有正确定义它。不过,它粗略地用作“隐式声明”的对应物,但(与术语本身结合使用时) ) 足以隐含地定义它。)
  • 如果定义了一些构造函数,他希望所有对象都使用这些构造函数初始化……所以我认为这是对标准的一个受欢迎的修复。

标签: c++ c++17 backwards-compatibility c++20


【解决方案1】:

来自P1008 的摘要,导致更改的提案:

C++ 目前允许通过聚合初始化来初始化某些具有用户声明的构造函数的类型,绕过这些构造函数。结果是令人惊讶、混乱和错误的代码。本文提出了一种修复方法,使 C++ 中的初始化语义更安全、更统一且更易于教授。我们还讨论了此修复引入的重大更改。

他们给出的一个例子如下。

struct X {
  int i{4};
  X() = default;
};

int main() {
  X x1(3); // ill-formed - no matching c’tor
  X x2{3}; // compiles!
}

对我来说,很明显,提议的更改值得他们承担的向后不兼容。事实上,= default 聚合默认构造函数似乎不再是一个好习惯。

【讨论】:

  • 不应该是X x2{3},还是在第一个变量后用逗号代替分号?除此之外,我确实认为默认构造函数是一种好习惯,我宁愿认为聚合初始化是不好的做法(我们现在可以防止这种方式)。
  • @Aconcagua,为什么你认为聚合初始化是不好的做法?
  • @PatrickFromberg 对于聚合初始化,您依赖于成员顺序,因为我们没有像 C 中那样指定初始化器(至少)。想象一下,您需要添加一个新成员并进行对齐或其他原因,您需要将其放在其他一些之间。这将破坏聚合初始化。相比之下,构造函数可以适当地固定,因此它们更安全。
  • @T.C.真的吗?你介意提供一些参考吗? Bad 不过,在我看来……
【解决方案2】:

The reasoning from P1008 (PDF)最好从两个方向理解:

  1. 如果您让一个相对较新的 C++ 程序员坐在类定义前并问“这是一个聚合”,他们会正确吗?

聚合的常见概念是“没有构造函数的类”。如果Typename() = default; 在类定义中,大多数人会认为它具有构造函数。它将表现得像标准的默认构造函数,但类型仍然有一个。这是许多用户对这个想法的广泛概念。

聚合应该是一个纯数据类,能够让任何成员假设它被赋予的任何值。从这个角度来看,你没有义务给它任何类型的构造函数,即使你默认了它们。这就引出了下一个推理:

  1. 如果我的班级满足聚合的要求,但我不希望它成为聚合,我该怎么做?

最明显的答案是= default 默认构造函数,因为我可能是来自#1 组的人。显然,这是行不通的。

在 C++20 之前,您的选择是给类一些其他构造函数或实现特殊成员函数之一。这些选项都不是可口的,因为(根据定义)这不是您真正需要实现的东西;你这样做只是为了产生一些副作用。

后 C++20,显而易见的答案有效。

通过以这种方式更改规则,它使聚合和非聚合之间的区别可见。聚合没有构造函数;所以如果你想让一个类型成为一个聚合,你不要给它构造函数。

哦,还有一个有趣的事实:在 C++20 之前,这是一个聚合:

class Agg
{
  Agg() = default;
};

注意默认构造函数是private,所以只有拥有Agg 私有访问权限的人才能调用它...除非他们使用Agg{},绕过构造函数并且完全合法。

这个类的明确意图是创建一个可以复制的类,但只能从具有私有访问权限的那些中获取其初始构造。这允许转发访问控制,因为只有给定 Agg 的代码才能调用将 Agg 作为参数的函数。只有有权访问Agg 的代码才能创建一个。

或者至少,它应该是这样的。

现在你可以更有针对性地解决这个问题,如果默认/删除的构造函数没有公开声明,它就是一个聚合。但这感觉更加不一致。有时,具有明显声明的构造函数的类是一个聚合,有时则不是,具体取决于该可见声明的构造函数的位置。

【讨论】:

  • 我明白了...好的,这里有一些令人信服的论据,谢谢。如果您将= default 更改为= delete,您的最后一个示例会更好,因为这更能表达意图(并且Agg{} 仍然在C++ 17 中工作,呃......不知道这一点)。
  • @sebrockm:不是这样。如果您删除默认构造函数,则 nobody 应该能够创建该类型的对象(在复制之外)。如果您将其设为私有的默认构造函数,那么只有具有私有访问权限的人才能创建一个(同样,在复制之外)。
  • 正确,意图略有不同。尽管如此,“没有人应该创建它(但仍然每个人都可以)”比“只有一些选定的人应该创建它(但仍然每个人都可以)”。这就是我的观点:)
  • @sebrockm:但这很容易被以下论点反驳,“如果没有人应该能够创建对象......为什么类型存在?”虽然您可以指出各种将类型纯粹用作计算引擎的元编程工具,但它们都没有主动禁止创建此类类型的实例,如果有人这样做,它们在概念上也不会被破坏。那么,为什么能够创建一个无法实例化的类型,而不仅仅是一个实例化无用的类型很重要呢?
  • 我想我们在这里谈论的是不同的事情。我不建议删除默认 c'tor,因此表达“永远不会创建”是合理的做法。不过,如果我这样做了,我希望绝对不会有此类的实例。但相反,每个人都可以创建它。对我来说,这种行为甚至比您 确实 期望创建对象的示例更违反直觉(只是 可以做到,这是一个惊喜,但不是i>那它可以做到在所有)。
【解决方案3】:

在 C++20 中实现一个不那么令人惊讶的聚合

为了与所有读者保持一致,让我们首先提到聚合类类型构成了一个特殊的类类型家族,特别是可以通过聚合初始化来初始化,使用direct-list-initcopy-list-init,分别为 T aggr_obj{arg1, arg2, ...}T aggr_obj = {arg1, arg2, ...}

管理一个类是否是一个聚合的规则并不完全直截了当,尤其是在 C++ 标准的不同版本之间规则一直在变化的情况下。在这篇文章中,我们将讨论这些规则以及它们在标准版本中从 C++11 到 C++20 的变化。

在我们访问相关的标准段落之前,请考虑以下人为类类型的实现:

namespace detail {
template <int N>
struct NumberImpl final {
    const int value{N};
    // Factory method for NumberImpl<N> wrapping non-type
    // template parameter 'N' as data member 'value'.
    static const NumberImpl& get() {
        static constexpr NumberImpl number{};
        return number;
    }

private:
    NumberImpl() = default;
    NumberImpl(int) = delete;
    NumberImpl(const NumberImpl&) = delete;
    NumberImpl(NumberImpl&&) = delete;
    NumberImpl& operator=(const NumberImpl&) = delete;
    NumberImpl& operator=(NumberImpl&&) = delete;
};
}  // namespace detail

// Intended public API.
template <int N>
using Number = detail::NumberImpl<N>;

设计意图是创建一个不可复制、不可移动的单例类模板,该模板将其单个非类型模板参数包装到公共常量数据成员中,并且每个实例化的单例对象是唯一的可以为这个特定的类专业化创建。作者定义了一个别名模板Number,只是为了禁止API的用户显式地专门化底层detail::NumberImpl类模板。

忽略这个类模板的实际有用性(或者,更确切地说,无用),作者是否正确地实现了它的设计意图?或者,换句话说,给定下面的函数wrappedValueIsN,用作公开预期的数字别名模板设计的验收测试,该函数是否总是返回true

template <int N>
bool wrappedValueIsN(const Number<N>& num) {
    // Always 'true', by design of the 'NumberImpl' class?
    return N == num.value;
}

我们将假设没有用户通过专门化语义隐藏的detail::NumberImpl 滥用界面来回答这个问题,在这种情况下,答案是:

  • C++11:是的
  • C++14:否
  • C++17:否
  • C++20:是的

主要区别在于类模板detail::NumberImpl(对于它的任何非显式特化)在 C++14 和 C++17 中是一个聚合,而在 C++11 和 C++17 中它不是一个聚合C++20。如上所述,如果对象是聚合类型,则使用 direct-list-init 或 copy-list-init 初始化对象将导致聚合初始化。因此,可能看起来像 value-initialization(例如这里的 Number&lt;1&gt; n{})——我们可能期望它会产生 zero-initialization 的效果,然后是 default-初始化 作为 user-declared 但不存在 user-provided 默认构造函数 - 或 direct-initialization (例如Number&lt;1&gt;n{2} 这里) 的类类型对象实际上会绕过任何构造函数,即使是已删除的构造函数,如果类类型是聚合的话。

struct NonConstructible {
    NonConstructible() = delete;
    NonConstructible(const NonConstructible&) = delete;
    NonConstructible(NonConstructible&&) = delete;
};

int main() {
    //NonConstructible nc;  // error: call to deleted constructor

    // Aggregate initialization (and thus accepted) in
    // C++11, C++14 and C++17.
    // Rejected in C++20 (error: call to deleted constructor).
    NonConstructible nc{};
}

因此,我们可以通过聚合初始化绕过 detail::NumberImpl 的私有和删除的 user-declared 构造函数,使 C++14 和 C++17 中的 wrappedValueIsN 验收测试失败,特别是我们显式地为单个 value 成员提供值,从而覆盖指定的成员初始化程序 (... value{N};),否则将其值设置为 N

constexpr bool expected_result{true};
const bool actual_result =
    wrappedValueIsN(Number<42>{41}); // false
                           // ^^^^ aggr. init. int C++14 and C++17.

请注意,即使 detail::NumberImpl 声明了一个私有且显式默认的析构函数(~NumberImpl() = default;private 访问指定器),我们仍然可以以内存泄漏为代价,通过例如使用聚合初始化 (wrappedValueIsN(*(new Number&lt;42&gt;{41}))) 动态分配(并且从不删除)detail::NumberImpl 对象。

但是为什么是 detail::NumberImpl 是 C++14 和 C++17 中的聚合,为什么不是 C++11 和 C 中的聚合++20?我们将转向不同标准版本的相关标准段落寻求答案。

C++11 中的聚合

[dcl.init.aggr]/1 涵盖了管理类是否为聚合的规则,我们在其中引用 N3337 (C++11 + editorial fixes) 来表示 C++11 [强调我的]:

聚合是一个数组或一个类(子句 [class]),没有 用户提供的构造函数([class.ctor]),否 非静态数据成员的大括号或等号初始化器 ([class.mem]),没有私有或受保护的非静态数据成员(子句 [class.access]),没有基类(子句 [class.derived]),也没有 虚函数([class.virtual])。

强调的部分是与此答案的上下文最相关的部分。

用户提供的功能

detail::NumberImpl声明四个构造函数,因此它有四个用户声明构造函数,但它不提供定义对于这些构造函数中的任何一个;它在构造函数的第一个声明中使用 explicitly-defaultedexplicitly-deleted 函数定义,分别使用 defaultdelete 关键字。

根据[dcl.fct.def.default]/4 的规定,在第一次声明时定义显式默认或显式删除的函数不计入用户提供的函数 [摘录,强调强>我的]:

[…] 如果一个特殊的成员函数是用户声明的,并且在其第一次声明时没有显式默认或删除,则它是用户提供的。 […]

因此,detail::NumberImpl 满足关于没有用户提供的构造函数的聚合类要求。

对于一些额外的聚合混淆(适用于 C++11 到 C++17),其中显式默认定义是离线提供的,请参阅my other answer here

指定的成员初始化器

尽管detail::NumberImpl 类没有用户提供的构造函数,但它确实使用了大括号或相等初始化器(通常称为指定成员初始化器 ) 用于单个非静态数据成员值。这是 detail::NumberImpl在 C++11 中不是聚合的唯一原因。

C++14 中的聚合

对于 C++14,我们再次转向 [dcl.init.aggr]/1,现在指的是 N4140 (C++14 + editorial fixes),这与 C++11 中的相应段落几乎相同,除了关于 brace-or 的段-equal-initializers 已被移除 [emphasis mine]:

聚合是一个数组或一个类(子句 [class]),没有 用户提供的构造函数([class.ctor]),没有私有或受保护 非静态数据成员(子句 [class.access]),无基类 (子句 [class.derived]),并且没有虚函数([class.virtual])。

因此,detail::NumberImpl满足了在 C++14 中作为聚合的规则,从而允许规避所有私有、默认或删除的用户声明 通过聚合初始化的构造函数。

一旦我们在一分钟内到达 C++20,我们将回到关于用户提供的构造函数的一贯强调的部分,但我们将首先访问 C++17 中的一些 explicit 困惑.

C++17 中的聚合

在 C++17 中聚合再次改变了它的形式,现在允许聚合从基类公开派生,但有一些限制,以及禁止聚合的 explicit 构造函数。 [dcl.init.aggr]/1 来自 N4659 ((March 2017 post-Kona working draft/C++17 DIS),声明 [强调 我的]:

聚合是一个数组或一个类

  • (1.1) 没有用户提供的、显式的或继承的构造函数([class.ctor]),
  • (1.2) 没有私有或受保护的非静态数据成员(子句 [class.access]),
  • (1.3) 没有虚函数,并且
  • (1.4) 没有虚拟、私有或受保护的基类 ([class.mi])。

关于explicit 的部分在这篇文章的上下文中很有趣,因为我们可以通过更改@987654368 的私有用户声明的显式默认默认构造函数的声明来进一步增加跨标准发布的总体波动性@来自:

template <int N>
struct NumberImpl final {
    // ...
private:
    NumberImpl() = default;
    // ...
};

template <int N>
struct NumberImpl final {
    // ...
private:
    explicit NumberImpl() = default;
    // ...
};

detail::NumberImpl 在 C++17 中不再是聚合,而在 C++14 中仍然是聚合。将此示例表示为(*)。除了 copy-list-initialization 和一个 empty braced-init-list(在my other answer here 中查看更多详细信息):

struct Foo {
    virtual void fooIsNeverAnAggregate() const {};
    explicit Foo() {}
};

void foo(Foo) {}

int main() {
    Foo f1{};    // OK: direct-list-initialization

    // Error: converting to 'Foo' from initializer
    // list would use explicit constructor 'Foo::Foo()'
    Foo f2 = {};
    foo({});
}

(*) 中显示的情况是explicit 实际上对没有参数的默认构造函数产生影响的唯一情况。

C++20 中的聚合

从 C++20 开始,特别是由于 P1008R1 的实现(禁止使用用户声明的构造函数进行聚合),上面提到的大多数经常令人惊讶的聚合行为已经得到解决,特别是通过不再允许聚合具有 user-declared 构造函数,对于一个类作为聚合的要求比仅仅禁止 user-provided 构造函数更严格。我们再次转向[dcl.init.aggr]/1,现在指的是N4861 (March 2020 post-Prague working draft/C++20 DIS),其中指出[强调我的]:

聚合是一个数组或一个类([class])

  • (1.1) 没有用户声明的,或继承的构造函数 ([class.ctor]),
  • (1.2) 没有私有或受保护的非静态数据成员 ([class.access]),
  • (1.3) 没有虚函数 ([class.virtual]),并且
  • (1.4) 没有虚拟、私有或受保护的基类 ([class.mi])。

我们可能还注意到,关于 explicit 构造函数的部分已被删除,现在是多余的,因为如果我们甚至可能不声明构造函数,我们就无法将它标记为 explicit

避免总体意外

上述所有示例都依赖于具有公共非静态数据成员的类类型,这通常被认为是设计“非 POD 类”类的反模式。根据经验,如果您想避免设计一个无意中聚合的类,只需确保其非静态数据成员中的至少一个(通常甚至全部)是私有的(/受保护的)。对于由于某种原因无法应用的情况,并且您仍然不希望该类成为聚合,请确保转向相应标准的相关规则(如上所列)以避免编写一个不便携wrt是否是不同 C++ 标准版本的聚合。

【讨论】:

    【解决方案4】:

    实际上,MSDN 在以下文档中解决了您的问题:

    Modified specification of aggregate type

    在 Visual Studio 2019 中,在 /std:c++latest 下,具有任何用户声明的构造函数(例如,包括声明为 = default 或 = delete 的构造函数)的类不是聚合。以前,只有用户提供的构造函数会取消一个类作为聚合的资格。此更改对如何初始化此类类型施加了额外的限制。

    【讨论】:

      猜你喜欢
      • 2019-11-30
      • 2016-02-05
      • 2021-09-01
      • 1970-01-01
      • 1970-01-01
      • 2013-02-01
      • 1970-01-01
      • 2020-03-24
      相关资源
      最近更新 更多