如果显式默认或删除了构造函数,为什么自C ++ 20起聚合初始化不再起作用?

seb*_*ckm 21 c++ backwards-compatibility c++17 c++20

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

我现在遇到一个错误,以前没有发生过,可以通过以下几行代码来重现:

struct Foo
{
    Foo() = default;
    int bar;
};
auto test = Foo { 0 };
Run Code Online (Sandbox Code Playgroud)

错误是

(6):错误C2440:“正在初始化”:无法从“初始化列表”转换为“ Foo”

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

该项目用/std:c++latest标志编译。我把它复制在了哥德螺栓上。如果我将其切换到/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的变化背后的原因什么?向后兼容的突破是故意的吗?是否有一些折衷办法,例如“确定,我们在这里破坏了向后兼容性,但这是为了更大的利益。”吗?这个更大的好处是什么?

lub*_*bgr 21

导致变更的提案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!
}
Run Code Online (Sandbox Code Playgroud)

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

  • @PatrickFromberg 对于聚合初始化,您依赖于成员顺序,因为我们还没有像 C 中那样指定的初始化器(至少)。假设您需要添加一个新成员,并且出于对齐或其他原因,您需要放置它在其他一些人之间。这将破坏聚合初始化。相比之下,构造函数可以适当修复,因此它们更安全。 (2认同)

dfr*_*fri 15

C++20 中的聚合不再令人意外

\n

为了与所有读者达成共识,我们首先要提到聚合类类型构成了一个特殊的类类型系列,特别是可以通过聚合初始化、使用direct-list-initcopy-list- 来初始化。分别是initT aggr_obj{arg1, arg2, ...}T aggr_obj = {arg1, arg2, ...}

\n

控制类是否为聚合的规则并不完全简单,特别是因为这些规则在 C++ 标准的不同版本之间一直在变化。在这篇文章中,我们\xe2\x80\x99 将回顾这些规则以及它们在从 C++11 到 C++20 的标准版本中是如何变化的。

\n

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

\n
namespace detail {\ntemplate <int N>\nstruct NumberImpl final {\n    const int value{N};\n    // Factory method for NumberImpl<N> wrapping non-type\n    // template parameter \'N\' as data member \'value\'.\n    static const NumberImpl& get() {\n        static constexpr NumberImpl number{};\n        return number;\n    }\n\nprivate:\n    NumberImpl() = default;\n    NumberImpl(int) = delete;\n    NumberImpl(const NumberImpl&) = delete;\n    NumberImpl(NumberImpl&&) = delete;\n    NumberImpl& operator=(const NumberImpl&) = delete;\n    NumberImpl& operator=(NumberImpl&&) = delete;\n};\n}  // namespace detail\n\n// Intended public API.\ntemplate <int N>\nusing Number = detail::NumberImpl<N>;\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n

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

\n
template <int N>\nbool wrappedValueIsN(const Number<N>& num) {\n    // Always \'true\', by design of the \'NumberImpl\' class?\n    return N == num.value;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

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

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

主要区别在于,类模板detail::NumberImpl(对于其任何非显式特化)是 C++14 和 C++17 中的聚合,而它不是 C++11 和 C++20 中的聚合。如上所述,如果对象是聚合类型,则使用 direct-list-init 或 copy-list-init 初始化对象将导致聚合初始化。因此,看起来像值初始化(例如Number<1> n{}这里)\xe2\x80\x94,我们可能期望它具有零初始化的效果,然后是默认初始化作为用户声明但不是用户提供的默认构造函数存在\xe2如果类类型是聚合,则类类型对象的\x80\x94 或直接初始化(例如此处)实际上将绕过任何构造函数,甚至删除的构造函数。Number<1>n{2}

\n
struct NonConstructible {\n    NonConstructible() = delete;\n    NonConstructible(const NonConstructible&) = delete;\n    NonConstructible(NonConstructible&&) = delete;\n};\n\nint main() {\n    //NonConstructible nc;  // error: call to deleted constructor\n\n    // Aggregate initialization (and thus accepted) in\n    // C++11, C++14 and C++17.\n    // Rejected in C++20 (error: call to deleted constructor).\n    NonConstructible nc{};\n}\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n
constexpr bool expected_result{true};\nconst bool actual_result =\n    wrappedValueIsN(Number<42>{41}); // false\n                           // ^^^^ aggr. init. int C++14 and C++17.\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n

为什么 detail::NumberImplC++14 和 C++17 中是聚合,而C++11 和 C++20 中不是聚合呢?我们将根据不同标准版本的相关标准段落来寻找答案。

\n

C++11 中的聚合

\n

[dcl.init.aggr]/1涵盖了管理类是否为聚合的规则,其中我们参考C++11 的 N3337(C++11 + 编辑修复) [重点是我的]:

\n
\n

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

\n
\n

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

\n

用户提供的函数

\n

该类detail::NumberImpl确实声明了四个构造函数,因此它具有四个用户声明的构造函数,但它不提供任何这些构造函数的定义;它在构造函数\xe2\x80\x99 第一个声明中使用显式默认显式删除的default函数定义,分别使用和delete关键字。

\n

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

\n
\n

[\xe2\x80\xa6] 如果特殊成员函数是用户声明的并且在第一次声明时未显式默认或删除,则该特殊成员函数是用户提供的。[\xe2\x80\xa6]

\n
\n

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

\n

对于一些额外的聚合混乱(适用于 C++11 到 C++17),其中显式默认定义是不符合规定的,请参阅我的其他答案

\n

指定成员初始值设定项

\n

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

\n

C++14 中的聚合

\n

对于 C++14,我们再次转向[dcl.init.aggr]/1,现在指的是N4140 (C++14 + 编辑修复),它与 C++11 中的相应段落几乎相同,除了关于大括号或等于初始化器的部分已被删除[强调我的]:

\n
\n

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

\n
\n

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

\n

一旦我们在一分钟内到达 C++20,我们将回到一直强调的关于用户提供的构造函数的部分,但我们将首先解决explicitC++17 中的一些困惑。

\n

C++17 中的聚合

\n

正如其形式一样,聚合在 C++17 中再次发生变化,现在允许聚合从基类公开派生,但有一些限制,并禁止explicit聚合的构造函数。[dcl.init.aggr]/1来自N4659((2017 年 3 月后 Kona 工作草案/C++17 DIS),指出[强调我的]:

\n
\n

聚合是一个数组或一个类

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

about 中的部分在本文中很有趣,因为我们可以通过更改fromexplicit的私有用户声明的显式默认默认构造函数的声明来进一步增加总体跨标准版本的波动性:detail::NumberImpl

\n
template <int N>\nstruct NumberImpl final {\n    // ...\nprivate:\n    NumberImpl() = default;\n    // ...\n};\n
Run Code Online (Sandbox Code Playgroud)\n

\n
template <int N>\nstruct NumberImpl final {\n    // ...\nprivate:\n    explicit NumberImpl() = default;\n    // ...\n};\n
Run Code Online (Sandbox Code Playgroud)\n

其效果detail::NumberImpl是在 C++17 中不再是聚合,但在 C++14 中仍然是聚合。将此示例表示为(*)。除了使用空的花括号初始化列表进行复制列表初始化之外(请参阅我的其他答案中的更多详细信息):

\n
struct Foo {\n    virtual void fooIsNeverAnAggregate() const {};\n    explicit Foo() {}\n};\n\nvoid foo(Foo) {}\n\nint main() {\n    Foo f1{};    // OK: direct-list-initialization\n\n    // Error: converting to \'Foo\' from initializer\n    // list would use explicit constructor \'Foo::Foo()\'\n    Foo f2 = {};\n    foo({});\n}\n
Run Code Online (Sandbox Code Playgroud)\n

中所示的情况(*)是唯一explicit对不带参数的默认构造函数实际产生影响的情况。

\n

C++20 中的聚合

\n

从 C++20 开始,特别是由于P1008R1禁止使用用户声明的构造函数进行聚合)的实现,上面提到的大多数经常令人惊讶的聚合行为已经得到解决,特别是不再允许聚合具有用户声明的构造函数,对类作为聚合的要求比仅仅禁止用户提供的构造函数更严格。我们再次转向[dcl.init.aggr]/1,现在指的是N4861(2020 年 3 月布拉格后工作草案/C++20 DIS),其中指出[强调我的]:

\n
\n

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

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

我们还可能注意到,有关构造函数的部分explicit已被删除,现在是多余的,因为我们无法标记构造函数,就好像explicit我们甚至无法声明它一样。

\n

避免总体意外

\n

上面的所有示例都依赖于具有公共非静态数据成员的类类型,这通常被认为是 \xe2\x80\x9cnon-POD-like\xe2\x80\x9d 类设计的反模式。根据经验,如果您\xe2\x80\x99d 希望避免设计一个无意中聚合的类,只需确保其至少一个(通常甚至全部)非静态数据成员是私有的(/protected )。如果由于某种原因无法应用此方法,并且您仍然不\xe2\x80\x99 不希望该类成为聚合,请确保参阅相应标准的相关规则(如上所述)以避免编写不可移植的类,不能作为聚合体或不能在不同的 C++ 标准版本上移植。

\n


Nic*_*las 13

从两个方向可以最好地理解P1008(PDF)的推理

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

集合的常见概念是“没有构造函数的类”。如果Typename() = default;在类定义中,大多数人将其视为具有构造函数。它的行为类似于标准的默认构造函数,但是类型仍然具有一个。那是许多用户的广泛想法。

聚合应该是一类纯数据,能够使任何成员采用它给出的任何值。从这个角度来看,即使您默认了它们,您也无须为其提供任何类型的构造函数。这将我们带入下一个推理:

  1. 如果我的课程满足汇总的要求,但我不希望它成为汇总,那么我该怎么做?

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

在C ++ 20之前的版本中,您的选择是为类提供其他构造函数或实现特殊的成员函数之一。这两个选项都不是可口的,因为(根据定义)这不是您实际需要实现的东西。您只是在做一些副作用。

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

通过以这种方式更改规则,可以使聚合和非聚合之间的差异可见。集合没有构造函数。因此,如果您希望类型成为聚合类型,则不要给它构造函数。

哦,这是一个有趣的事实:C ++ 20之前的版本,这是一个合计:

class Agg
{
  Agg() = default;
};
Run Code Online (Sandbox Code Playgroud)

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

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

至少,这就是应该的样子。

现在,您可以说得更明确一些,以解决这个问题:如果未公开声明默认/删除的构造函数,这是一个汇总。但这感觉更加不一致。有时,具有可见声明的构造函数的类是一个聚合,有时不是,则取决于该可见声明的构造函数的位置。

  • @sebrockm:但这很容易被以下论点所反驳:“如果没有人能够创建对象……为什么类型根本存在?” 而且,尽管您可以指向各种纯粹将类型用作计算引擎的元编程工具,但它们都没有主动*禁止*创建此类类型的实例,如果有人这样做,它们在概念上也不会被破坏。那么,为什么能够创建一个无法实例化的类型而不是仅实例化没有用的类型为何重要呢? (2认同)