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

时间:2011-09-14 05:08:52

标签: c++

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;
}

5 个答案:

答案 0 :(得分:65)

原因是如果类没有用户定义的构造函数,那么它可以是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-qualified 类型。也就是说,如果没有指定初始化程序,则应对非POD对象进行默认初始化。什么是默认初始化?对于非POD,规范说(§8.5/ 5),

  

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

它简单地讨论了T的默认构造函数,无论其用户定义的还是编译器生成的都是无关紧要的。

如果您对此有所了解,请了解下一条规范((§8.5/ 9),

  

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

所以这个文本暗示,程序将是格式错误如果对象是 const-qualified POD类型,并且没有指定初始化程序(因为POD是没有默认初始化):

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

顺便说一句,this compiles fine,因为它是非POD,并且可以默认初始化

答案 1 :(得分:11)

我的纯粹猜测,但也认为其他类型也有类似的限制:

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 ++倾向于在某些方面信任程序员。

答案 2 :(得分:9)

这被视为缺陷(针对所有版本的标准),并由Core Working Group (CWG) Defect 253解决。标准的新措辞在http://eel.is/c++draft/dcl.init#7

中说明
  

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

     
      
  • T的每个直接非变量非静态数据成员M具有默认成员初始值设定项,或者如果M是类型X(或其数组),则X   是const-default-constructible,
  •   
  • 如果T是与至少一个非静态数据成员的并集,则只有一个变体成员具有默认成员初始值设定项,
  •   
  • 如果T不是联合,对于每个具有至少一个非静态数据成员(如果有)的匿名联合成员,只有一个非静态数据   member具有默认成员初始值设定项,
  •   
  • 每个可能构造的T的基类都是const-default-constructible。
  •   
     

如果程序要求对象的默认初始化   const限定类型T,T应该是一个const-default-constructible类   类型或数组。

这个措辞本质上意味着明显的代码有效。如果您初始化所有基础和成员,无论您是否拼写任何构造函数,都可以说A const a;

struct A {
};
A const a;
自4.6.4以来,gcc已经接受了这一点。 clang从3.9.0开始就接受了这一点。 Visual Studio也接受这一点(至少在2017年,不确定是否更早)。

答案 3 :(得分:1)

恭喜,您已经发明了一个案例,其中const声明不需要任何用户定义的构造函数,也没有初始化器。

现在,您是否可以对涵盖您案件的规则进行合理的重新措辞,但仍然会将案件视为非法非法行为?它是不到5或6段?在任何情况下应该如何应用它是否容易和明显?

我认为提出一个允许你创建的声明有意义的规则非常困难,并且确保规则可以以一种在阅读代码时对人们有意义的方式应用更加困难。在大多数情况下,我更倾向于采用一种有限制性的规则来处理一个难以理解和应用的非常微妙和复杂的规则。

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

答案 4 :(得分:0)

我在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(因为它拥有一个不确定的值)。因此,此规则通过强制您 将初始化程序选择加入危险行为(通过添加用户提供的构造函数)来阻止您创建此类事物。 / p>

我认为最好有一个[[uninitialized]]这样的属性来告诉编译器何时您不打算初始化对象。这样,我们就不会被迫使我们的类不能轻易被构造为可避免这种情况的发生。