it-swarm.cn

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

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

这被认为是一个缺陷(针对所有版本的标准),并由 核心工作组(CWG)缺陷253 解决。标准的新措辞在 http://eel.is/c++draft/dcl.init#7

如果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-constructible类类型或其数组。

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

struct A {
};
A const a;

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

9
David Stone

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

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

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

如果您对此有所了解,那么请理解下一个规范的内容((§8.5/ 9),

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

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

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

顺便说一下, 这个编译很好 ,因为它的非POD,可以是默认初始化

65
Nawaz

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

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

11
Luc Danton

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

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

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

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

1
Omnifarious

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

考虑两个类:A有一个 user-declared constructor,B有一个 user-provided constructor:

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(因为它包含不确定的值)。因此,此规则会阻止您通过添加用户提供的构造函数来强制您或者添加初始化 opt-in到危险行为,从而阻止您创建此类内容。

我认为当你故意不初始化一个对象时,有一个像[[uninitialized]]这样的属性告诉编译器会很好。然后,我们不会被迫使我们的课程不是简单的默认构造,以绕过这个角落的情况。

0
Oktalist