当T不是POD类型时,const T{}可以正常工作,但const T会失败。

25
首先,我有一个结构体,其中包含一个具有默认值的值。
struct S {
    int a = 1;
};

当它是非常量/非constexpr时,gcc和clang都可以默认构造此类型。在两者下,std :: is_pod<S> :: value均为false。其奇怪的行为如下:

S s1; // works under both
const S s2{}; // works under both
const S s3; // only works in gcc, clang wants a user-provided constructor

以下任何尝试都不会影响clang:

struct S {
    int a = 1;
    constexpr S() = default; // defaulted ctor
    virtual void f() { } // virtual function, not an aggregate
  private:
    int b = 2; // private member, really not an aggregate
};

唯一让它生效的方法是显式添加constexpr S() { }。 当类型不是聚合体时,const S s;失败而const S s{};成功,这对我来说似乎非常错误。 标准让我认为Clang是正确的,N4296: 8.5/7:

如果程序要求使用const限定类型T的默认初始化对象,则T必须是具有用户提供的默认构造函数的类类型。

那么为什么gcc会允许这种情况,即使该类型不是POD或聚合体,S{};也不能进行默认初始化呢?

2
http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#253;同时`{}`用于非聚合体的值初始化。 - T.C.
@David 我认为这里还有一些关于 = default 的内容。 - Ryan Haining
我刚刚回答了我标记为重复的问题。这里的问题在于原始措辞要求构造函数是“用户提供的”。更新后的措辞提供了“用户提供”的例外情况,这将允许您的用例编译。= default不起作用的原因是标准将用户提供定义为“如果它是用户声明的并且在其第一次声明中未明确默认或删除,则函数是用户提供的”。 - David Stone
2个回答

16
const S s3;

被[dcl.init]/12覆盖:

如果没有为对象指定初始化程序,则该对象将进行默认初始化。

因此,根据您的引用要求,必须存在用户提供的默认构造函数。可以像这样添加一个:

struct S {
    int a = 1;
    constexpr S(){}
};

然后声明编译正常,特别是当类型不是聚合类型时。在您的情况下,S是一个聚合体,这就是为什么const S s {}有效的原因。对于const S s{}采用了聚合初始化方式,一切都很好。如果S不是一个聚合体,
对于类型T的对象或引用的列表初始化定义如下:
  • 如果T是聚合体,则执行聚合初始化。
  • 否则,如果初始化器列表没有元素并且T是具有默认构造函数的类类型,则对象进行值初始化。
现在考虑值初始化的定义:
将类型T的对象进行值初始化意味着:
  • 如果T是一个(可能是CV限定的)类类型(第9条款),它没有默认构造函数(12.1)或者该构造函数是用户提供或删除的,则对象被默认初始化;
  • 如果T是一个(可能是CV限定的)类类型,没有用户提供或删除的默认构造函数, 则对象进行零初始化,并检查默认初始化的语义约束,如果T有一个非平凡的默认构造函数,则对象被默认初始化。
虽然默认构造函数是非平凡的,因为一个成员具有初始化程序([class.ctor]/4.9),但这是无关紧要的,因为约束检查是进行的。因此采用默认初始化,并且这一行:
const S s{};

和其他技术一样,这个技术的有效性(或无效性)同样适用。

const S t;

那么,为什么GCC允许这样做呢?

原因如下:

  1. 从当前标准的角度来看,GCC不符合标准;请参见上文。

  2. 有一个活跃的CWG问题,编号为#253,创建于15年前,涵盖了类似的情况。这个问题在2011年的一次会议中得出了最终结论:

    如果隐式默认构造函数初始化了所有子对象,则不需要任何初始化器。

    对于S的隐式默认构造函数而言,就是这种情况,因此您的所有行都是有效的。

  3. GCC开发人员(例如这里)暗示,由于委员会基本上同意了上述决议,GCC的当前行为是可行的,不应进行调整。因此,有人可以很好地争辩说GCC是正确的,而标准是错误的。


当你知道标准将被更改以允许这样的代码时,投资时间更改编译器以破坏用户代码有什么意义呢? - T.C.
请参阅Jonathan Wakely在这里的评论:https://dev59.com/pWEh5IYBdhLWcg3w5243#RaifEYcBWogLw_1bfwl9。 - T.C.
我认为我可能同意gcc,除非它需要一个“--pedantic-including-broken-parts-of-standard-in-question”模式。 - Yakk - Adam Nevraumont
1
@Yakk -reasonably-pedantic? - Columbo
@Columbo --unreasonably-pedantic -- 这个想法是在 C++11 模式下,它将拒绝后 C++11 的“错误修复”:尽可能地重现实际的 C++11 标准,包括其缺陷。与 --pedantic 相反,其中包括标准的错误修复(而不是扩展)会发出警告。扩展和针对未解决问题的预防性错误修复之间的界限无疑是模糊的。 - Yakk - Adam Nevraumont
显示剩余2条评论

5

看起来gcc基于DR 253,尽管这个问题尚未解决。我们可以从以下gcc错误报告中看到:

这是设计上的问题,因为正如DR 253所示,规范标准存在缺陷。

gcc引入此更改的说法如下:

Core 234-允许没有初始化程序或用户提供默认构造函数的const对象,如果默认构造函数初始化了所有子对象。

所以从技术上讲,clang 是正确的,而 gcc 不符合标准,但是他们似乎相信 DR 253 会被解决并有利于他们。如果主要关注的是不确定的初始值,这就完全说得通了,就我所知道的情况是如此。这个变化在 gcc 4.6 发布说明 中有记录:

In 4.6.0 and 4.6.1 G++ no longer allows objects of const-qualified type to be default initialized unless the type has a user-declared default constructor. In 4.6.2 G++ implements the proposed resolution of DR 253, so default initialization is allowed if it initializes all subobjects. Code that fails to compile can be fixed by providing an initializer e.g.

struct A { A(); };
struct B : A { int i; };
const B b = B();

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接