繁体   English   中英

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

[英]Why does C++ require a user-provided default constructor to default-construct a const object?

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

原因是如果类没有自定义构造函数,那么可以是POD,POD类默认是不初始化的。 那么如果你声明一个未初始化的 POD 常量对象,它有什么用呢? 所以我认为标准强制执行这个规则,以便对象实际上是有用的。

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,并且可以被默认初始化

纯属我的推测,但考虑到其他类型也有类似的限制:

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

那么也许我们可以制定一个规则,例如“必须在用户提供的默认构造函数中合理地初始化至少一个成员”,但是为了防止墨菲而花费了太多时间。 C++ 倾向于在某些方面信任程序员。

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

如果 T 的默认初始化会调用用户提供的 T 构造函数(不是从基类继承的),或者如果

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

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

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

struct A {
};
A const a;

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

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

考虑两个类: 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]]这样的属性来告诉编译器会很好。 这样我们就不会被迫让我们的类不能简单地默认构造来解决这个极端情况。 这个属性实际上已经被提出,但就像所有其他标准属性一样,它不强制要求任何规范行为,只是对编译器的一个提示。

恭喜,您已经发明了一种情况,在这种情况下,不需要为没有初始化器的const声明使用任何用户定义的构造const

现在你能想出一个合理的规则重新措辞,涵盖你的案件,但仍然使应该是非法的案件成为非法吗? 是少于 5 段还是 6 段? 如何在任何情况下应用它是否简单而明显?

我认为想出一个规则来让你创建的声明有意义真的很难,而在阅读代码更难时,确保可以以对人们有意义的方式应用规则。 我更喜欢一个在大多数情况下是正确的有点限制性的规则,而不是一个难以理解和应用的非常微妙和复杂的规则。

问题是,是否有令人信服的理由让规则更复杂? 是否有一些代码在其他情况下很难编写或理解,但如果规则更复杂,则可以更简单地编写?

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM