为什么C ++需要用户提供的默认构造函数来默认构造const对象?


99

C ++标准(第8.5节)说:

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

为什么?我想不出在这种情况下为什么需要用户提供的构造函数的任何原因。

struct B{
  B():x(42){}
  int doSomeStuff() const{return x;}
  int x;
};

struct A{
  A(){}//other than "because the standard says so", why is this line required?

  B b;//not required for this example, just to illustrate
      //how this situation isn't totally useless
};

int main(){
  const A a;
}

2
在您的示例中似乎不需要该行(请参见ideone.com/qqiXR),因为您已声明但未定义/初始化a,但gcc-4.3.4甚至在您接受时也接受了该行(请参见ideone.com/uHvFS
Ray Toal

上面的示例同时声明和定义a。如果注释掉该行,则Comeau产生错误“常量变量“ a”需要初始化程序-类“ A”没有显式声明的默认构造函数”。
卡鲁


4
这在C ++ 11中已修复,您可以编写const A a{}:)
Howard Lovatt

Answers:


10

这被认为是缺陷(相对于该标准的所有版本),并且已由核心工作组(CWG)缺陷253解决http://eel.is/c++draft/dcl.init#7中标准状态的新措词

如果T的默认初始化将调用用户提供的T的构造函数(不是从基类继承),或者如果T的默认初始化,则类类型T是const-default-constructible的

  • T的每个直接非可变非静态数据成员M都有一个默认成员初始化程序,或者,如果M是类类型X(或其数组),则X是const-default-constructible的,
  • 如果T是至少一个非静态数据成员的联合,则恰好一个变体成员具有默认成员初始化程序,
  • 如果T不是联合,则对于具有至少一个非静态数据成员(如果有)的每个匿名联合成员,恰好一个非静态数据成员具有默认成员初始化程序,并且
  • T的每个可能构造的基类都是const-default-constructible。

如果程序要求对const限定类型T的对象进行默认初始化,则T应为const-default可构造的类类型或其数组。

该措辞实质上意味着显而易见的代码有效。如果您初始化所有的基数和成员,则A const a;无论您如何拼写构造函数,都可以说。

struct A {
};
A const a;

从4.6.4开始,gcc已经接受了这一点。clang从3.9.0版开始接受此设置。Visual Studio也接受这一点(至少在2017年,不确定是否更快)。


3
但这struct A { int n; A() = default; }; const A a;在允许struct B { int n; B() {} }; const B b;的同时仍然禁止,因为新的措辞仍然说“用户提供”而不是“用户声明”,而我留下了疑问,为什么委员会选择从此DR中排除显式默认的默认构造函数,迫使我们做出如果我们想要具有未初始化成员的const对象,那么我们的类就很简单。
Oktalist's

1
有趣,但是仍然遇到了一个极端情况。与MyPOD作为一个POD structstatic MyPOD x;-依托零初始化(是正确的吗?)来设置成员变量(多个)适当-编译,但static const MyPOD x;没有。是否有任何机会,将得到解决吗?
约书亚·格林

66

原因是,如果该类没有用户定义的构造函数,则它可以是POD,并且默认情况下不会初始化POD类。因此,如果您声明未初始化的POD的const对象,那么它有什么用?因此,我认为标准强制执行此规则,以便该对象实际上是有用的。

struct POD
{
  int i;
};

POD p1; //uninitialized - but don't worry we can assign some value later on!
p1.i = 10; //assign some value later on!

POD p2 = POD(); //initialized

const POD p3 = POD(); //initialized 

const POD p4; //uninitialized  - error - as we cannot change it later on!

但是,如果您将课程设为非POD:

struct nonPOD_A
{
    nonPOD_A() {} //this makes non-POD
};

nonPOD_A a1; //initialized 
const nonPOD_A a2; //initialized 

请注意POD和非POD之间的区别。

用户定义的构造函数是使类成为非POD的一种方法。有几种方法可以做到这一点。

struct nonPOD_B
{
    virtual void f() {} //virtual function make it non-POD
};

nonPOD_B b1; //initialized 
const nonPOD_B b2; //initialized 

注意nonPOD_B没有定义用户定义的构造函数。编译它。它将编译:

并注释虚函数,然后按预期提供错误:


好吧,我想,您误解了这段话。它首先这样说(第8.5 / 9节):

如果没有为一个对象指定初始化程序,并且该对象是(可能是cv限定的)非POD类类型(或其数组),则该对象应被默认初始化;否则,默认为初始化。[...]

它讨论的是非POD类,可能是cv限定类型。也就是说,如果未指定初始化程序,则非POD对象应被默认初始化。什么是默认初始化的?对于非POD,规范指出(第8.5 / 5节),

默认初始化T类型的对象的意思是:
—如果T是非POD类类型(第9节),则调用T的默认构造函数(如果T没有可访问的默认构造函数,则初始化格式不正确);

它只是谈论T的默认构造函数,无论它是用户定义的还是编译器生成的都是无关紧要的。

如果您对此有所了解,请了解规范接下来的内容((§8.5/ 9),

[...]; 如果对象是const限定类型,则基础类类型应具有用户声明的默认构造函数。

因此,本文暗示,如果对象为const限定的 POD类型,并且未指定初始化程序,则程序将格式错误(因为POD未默认初始化):

POD p1; //uninitialized - can be useful - hence allowed
const POD p2; //uninitialized - never useful  - hence not allowed - error

顺便说一句,因为它不是POD,所以可以很好地编译,并且可以默认初始化


1
我相信您的最后一个示例是编译错误- nonPOD_B没有用户提供的默认构造函数,因此const nonPOD_B b2不允许该行。
卡鲁

1
使类成为非POD的另一种方法是给它一个不是POD的数据成员(例如,我B在问题中的结构)。但是在这种情况下,仍然需要用户提供的默认构造函数。
卡鲁

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

@Karu:我读过。似乎规范中还有其他段落,允许const通过调用编译器生成的default-constructor初始化非POD对象。
Nawaz

2
您的ideone链接似乎已断开,如果可以将此答案更新为C ++ 11/14,那就太好了,因为§8.5根本没有提到POD。
Oktalist,2014年

12

我纯粹是猜测,但请考虑其他类型也有类似的限制:

int main()
{
    const int i; // invalid
}

因此,此规则不仅是一致的,而且(递归地)可以防止统一的const(子)对象:

struct X {
    int j;
};
struct A {
    int i;
    X x;
}

int main()
{
    const A a; // a.i and a.x.j in unitialized states!
}

至于问题的另一面(允许使用默认构造函数的类型),我认为这种想法是,具有用户提供的默认构造函数的类型在构造后应始终处于某种合理的状态。请注意,这些规则适用于以下情况:

struct A {
    explicit
    A(int i): initialized(true), i(i) {} // valued constructor

    A(): initialized(false) {}

    bool initialized;
    int i;
};

const A a; // class invariant set up for the object
           // yet we didn't pay the cost of initializing a.i

然后,也许我们可以制定一条规则,例如“必须在用户提供的默认构造函数中明智地初始化至少一个成员”,但这花了太多时间来防范Murphy。C ++在某些方面倾向于信任程序员。


但是通过添加A(){},错误将消失,因此它无法阻止任何错误。该规则无法递归工作- X(){}该示例永远不需要。
卡鲁

2
好吧,至少是通过强迫程序员添加一个构造函数,他被迫
花点时间

@Karu我只回答了一半的问题-解决了这个问题:)
Luc Danton

4
@arne:唯一的问题是它是错误的程序员。试图实例化该类的人可能会对此想出所有想法,但他们可能无法修改该类。该类作者考虑了成员,发现它们都是由隐式默认构造函数明智地初始化的,因此从不添加成员。
卡鲁

3
我从标准的这一部分中学到的是“总是为非POD类型声明一个默认构造函数,以防某人想一天创建一个const实例”。这似乎有点矫kill过正。
卡鲁

3

我在Meetinging C ++ 2018上观看Timur Doumler的演讲时,我终于意识到为什么该标准在这里需要用户提供的构造函数,而不仅仅是用户声明的构造函数。它与价值初始化规则有关。

考虑两个类:A具有用户声明的构造函数,B具有用户提供的构造函数:

struct A {
    int x;
    A() = default;
};
struct B {
    int x;
    B() {}
};

乍一看,您可能会认为这两个构造函数的行为相同。但是,看看值初始化的行为方式有何不同,而只有默认初始化行为相同:

  • A a;是默认初始化:成员int x未初始化。
  • B b;是默认初始化:成员int x未初始化。
  • A a{};是值初始化:成员int x零初始化的
  • B b{};是值初始化:成员int x未初始化。

现在看看添加时会发生什么const

  • const A a;是默认初始化:由于问题中引用的规则,这是格式错误的
  • const B b;是默认初始化:成员int x未初始化。
  • const A a{};是值初始化:成员int x零初始化的
  • const B b{};是值初始化:成员int x未初始化。

未初始化的const标量(例如,int x成员)将无用:对它的写入格式不正确(因为是const),而从中读取是UB(因为它具有不确定的值)。因此,这条规则将阻止您创建这样的事情,迫使你要么添加一个初始化器通过添加一个用户提供的构造选入到危险的行为。

我认为,[[uninitialized]]当您有意不初始化对象时,有一个属性来告诉编译器会很好。这样,我们就不会被迫使我们的类不能被简单地默认构造,以解决这种情况。实际上已经提出了此属性,但是与所有其他标准属性一样,它不要求任何规范性行为,只是对编译器的提示。


1

恭喜,您发明了一种情况,其中不需要为const声明定义任何用户定义的构造函数,而无需使用任何初始化方法。

现在,您能否对涵盖您的案件但仍将应属非法的案件的规则重新进行合理的措词?少于5或6段吗?在任何情况下都应如何应用它是否简单明了?

我认为提出一个规则,使您创建的声明有意义是非常困难的,并且确保在阅读代码更加困难时,可以以对人们有意义的方式应用该规则。我宁愿使用在某些情况下应该做的正确的限制性规则,而不是很难理解和应用的细微差别和复杂规则。

问题是,该规则应该更复杂吗?是否存在一些本来很难编写或理解的代码,如果规则更复杂,则可以更简单地编写这些代码?


1
这是我的建议措辞:“如果程序要求对const限定类型T的对象进行默认初始化,则T应为非POD类类型。” 这将是非法的,const POD x;就像const int x;是非法的(有意义的,因为这对POD没用),但const NonPOD x;合法的(因为,它可能具有包含有用的构造函数/析构函数的子对象,或者本身具有有用的构造函数/析构函数) 。
卡鲁

@Karu-这种措辞可能有用。我已经习惯了RFC标准,所以觉得“ T be be”应该读为“ T must be”。但是,可以的。
2011年

@Karu-struct NonPod {int i; 虚拟void f(){}}?使const NonPod x没有意义;法律。
gruzovator

1
@gruzovator如果您有一个空的用户声明的默认构造函数,那还会有意义吗?我的建议只是试图消除对标准的毫无意义的要求。无论有没有它,仍然有无数种方法编写没有意义的代码。
卡鲁(Karu),

1
@Karu我同意你的看法。由于标准中有此规则,因此许多类必须具有用户定义的构造函数。我喜欢gcc行为。它允许例如struct NonPod { std::string s; }; const NonPod x;并且当NonPod是struct NonPod { int i; std::string s; }; const NonPod x;
gruzovator
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.