已删除默认构造函数.仍然可以创建对象..有时

Deleted default constructor. Objects can still be created... sometimes

本文关键字:创建对象 有时 删除 默认 构造函数      更新时间:2023-10-16

天真、乐观、噢。。所以c++11统一初始化语法的错误观点

我认为,由于C++11用户定义的类型对象应该使用新的{...}语法而不是旧的(...)语法来构建(除了为std::initializer_list和类似参数重载的构造函数(例如std::vector:size ctor vs 1 elem init_list ctor))。

好处是:没有狭窄的隐式转换,最麻烦的解析没有问题,一致性(?)。我认为没有问题,因为我认为它们是一样的(除了给出的例子)。

但事实并非如此。

一个纯粹疯狂的故事

{}调用默认构造函数。

除非:

  • 默认构造函数被删除,并且
  • 没有定义其他构造函数

然后看起来它相当于初始化对象的值?。。。即使对象已经删除了默认构造函数,{}也可以创建一个对象。这难道不符合删除构造函数的全部目的吗?

除非:

  • 该对象具有已删除的默认构造函数,并且
  • 定义的其他构造函数

然后它以call to deleted constructor失败。

除非:

  • 对象具有已删除的构造函数,并且
  • 没有定义其他构造函数,并且
  • 至少一个非静态数据成员

然后由于缺少字段初始值设定项而失败。

但是您可以使用{value}来构造对象。

好的,也许这与第一个异常(初始化对象的值)相同

除非:

  • 该类有一个已删除的构造函数
  • 并且类默认中的至少一个数据成员被初始化

那么{}{value}都不能创建对象。

我确信我错过了一些。具有讽刺意味的是,它被称为统一初始化语法。我再说一遍:UNIFORM初始化语法。

这是什么疯狂?

场景A

已删除默认构造函数:

struct foo {
  foo() = delete;
};
// All bellow OK (no errors, no warnings)
foo f = foo{};
foo f = {};
foo f{}; // will use only this from now on.

场景B

已删除默认构造函数,其他构造函数已删除

struct foo {
  foo() = delete;
  foo(int) = delete;
};
foo f{}; // OK

场景C

删除了默认构造函数,定义了其他构造函数

struct foo {
  foo() = delete;
  foo(int) {};
};
foo f{}; // error call to deleted constructor

场景D

已删除默认构造函数,未定义其他构造函数,数据成员

struct foo {
  int a;
  foo() = delete;
};
foo f{}; // error use of deleted function foo::foo()
foo f{3}; // OK

场景E

已删除默认构造函数,已删除T构造函数,T数据成员

struct foo {
  int a;
  foo() = delete;
  foo(int) = delete;
};
foo f{}; // ERROR: missing initializer
foo f{3}; // OK

场景F

已删除类数据成员初始值设定项中的默认构造函数

struct foo {
  int a = 3;
  foo() = delete;
};
/* Fa */ foo f{}; // ERROR: use of deleted function `foo::foo()`
/* Fb */ foo f{3}; // ERROR: no matching function to call `foo::foo(init list)`

当以这种方式查看事物时,很容易说对象的初始化方式完全混乱。

最大的区别来自foo的类型:它是否是聚合类型。

它是一个集合,如果它有:

  • 没有用户提供的构造函数(删除的或默认的函数不算作用户提供的)
  • 没有私有或受保护的非静态数据成员
  • 非静态数据成员没有大括号或相等的初始值设定项(从c++11到(在c++14中恢复)
  • 没有基类
  • 没有虚拟成员功能

因此:

  • 在场景A、B、D、E中:foo是一个聚合
  • 在场景C中:foo不是聚合
  • 场景F:
    • 在c++11中,它不是一个集合
    • 在c++14中,它是一个集合
    • g++还没有实现这一点,即使在C++14中也仍然将其视为非聚合。
      • 4.9没有实现这一点
      • 5.2.0
      • 5.2.1 ubuntu没有(可能是回归)

T类型对象的列表初始化的效果是:

  • 如果T是聚合类型,则执行聚合初始化。这涉及到场景A、B、D、E(以及C++14中的F)
  • 否则,T的构造函数分为两个阶段:
    • 所有采用std::initializer_list的构造函数
    • 否则[…]T的所有构造函数都参与重载解析[…]这会处理C(以及C++11中的F)

T类型对象的聚合初始化(场景A B D E(F c++14)):

  • 每个非静态类成员,按照在类定义中出现的顺序,都是从初始值设定项列表。(省略了数组引用)

TL;DR

所有这些规则看起来仍然非常复杂,令人头疼。我个人对这件事过于简化了(如果我因此射中了自己的脚,那就顺其自然:我想我会在医院住两天,而不是头痛几十天):

  • 对于聚合,每个数据成员都是从列表初始化器的元素初始化的
  • else调用构造函数

这难道不符合删除构造函数的全部目的吗?

嗯,我不知道,但解决方案是使foo不是一个聚合。不增加开销且不改变对象所用语法的最通用形式是使其从空结构继承:

struct dummy_t {};
struct foo : dummy_t {
  foo() = delete;
};
foo f{}; // ERROR call to deleted constructor

在某些情况下(我想根本没有非静态成员),替代方法是删除析构函数(这将使对象在任何上下文中都不可实例化):

struct foo {
  ~foo() = delete;
};
foo f{}; // ERROR use of deleted function `foo::~foo()`

这个答案使用从收集的信息

  • 用已删除的构造函数初始化C++14值

  • 什么是聚合和POD,它们是如何/为什么特别的?

  • 列表初始化

  • 聚合初始化
  • 直接初始化

非常感谢@M.M帮助更正和改进了这篇帖子。

让您感到困惑的是聚合初始化

正如您所说,使用列表初始化有好处也有缺点。(C++标准不使用术语"统一初始化")。

缺点之一是聚合的列表初始化行为与非聚合不同。此外,合计的定义随每个标准略有变化。


聚合不是通过构造函数创建的。(从技术上讲,它们实际上可能是,但这是一个很好的想法)。相反,在创建聚合时,会分配内存,然后根据列表初始值设定项中的内容按顺序初始化每个成员。

非聚合是通过构造函数创建的,在这种情况下,列表初始值设定项的成员是构造函数参数。

实际上,上面有一个设计缺陷:如果我们有T t1; T t2{t1};,那么目的是执行复制构建。但是,(在C++14之前)如果T是一个聚合,则会进行聚合初始化,并且t2的第一个成员是用t1初始化的。

这个缺陷在修改C++14的缺陷报告中得到了修复,所以从现在起,在我们进入聚合初始化之前,将检查复制构造。


C++14中聚合的定义是:

聚合是一个数组或类(第9条),它没有用户提供的构造函数(12.1),没有私有或受保护的非静态数据成员(第11条),没有基类(第10条),也没有虚拟函数(10.3)

在C++11中,非静态成员的默认值意味着类不是聚合;但是对于C++14,这一点发生了变化用户提供表示用户声明,但不是= default= delete


如果要确保构造函数调用不会意外执行聚合初始化,则必须使用( )而不是{ },并以其他方式避免MVP。

这些关于聚合初始化的情况对大多数人来说都是违反直觉的,并且是建议p1008的主题:禁止使用用户声明的构造函数进行聚合,该建议说:

C++目前允许一些具有用户声明构造函数的类型通过聚合进行初始化初始化,绕过那些构造函数。结果是代码令人惊讶、困惑,并且马车。本文提出了一种使C++中的初始化语义更安全、更统一、更灵活的修复方法,而且更容易教。我们还讨论了这个修复程序引入的突破性变化

并介绍了一些例子,这些例子与您介绍的案例很好地重叠:

struct X {
    X() = delete;
  };
 int main() {
    X x1;   // ill-formed - default c’tor is deleted
    X x2{}; // compiles!
}

显然,删除构造函数的目的是防止用户初始化类。然而,与直觉相反,这并不奏效:用户仍然可以初始化X通过聚合初始化,因为这完全绕过了构造函数。作者甚至可以显式删除所有默认、复制和移动构造函数,但仍然无法阻止客户端代码通过聚合初始化来实例化X。大多数C++开发人员都对显示此代码时的当前行为类X的作者也可以考虑创建默认构造函数私有的但是如果这个构造函数被赋予了一个默认的定义,这同样不会阻止类的聚合初始化(以及实例化):

struct X {
  private:
    X() = default;
  };
int main() {
    X x1;     // ill-formed - default c’tor is private
    X x2{};  // compiles!
  }

由于当前的规则,聚合初始化允许我们"默认构造"一个类,即使它实际上不是默认可构造的:

 static_assert(!std::is_default_constructible_v<X>);

将通过以上X的两个定义。

提议的变更为:

修改[dcl.init.agr]第1段如下:

聚合是具有的数组或类(第12条)

  • 未提供用户,显式 u̲;s;e;r-̲;d;e;c^3;l;a;r;e;d或继承构造函数(15.1),

  • 无私人或受保护的非静态数据成员(第14条),

  • 无虚拟功能(13.3)和

  • 没有虚拟、私有或受保护的基类(13.1)。

修改[dcl.init.agr]第17段如下:

[注意:聚合数组或聚合类可能包含类>>类型的元素,该类>>类型具有用户提供的u̲;s〲;e̲、r̲和d̲。这些聚合对象的初始化如15.6.1所述--尾注]

在附件C第C.5C++节和ISO C++2017中的[diff.cpp17]中添加以下内容:

C.5.6第11条:声明人[diff.cpp17.dcl.decl]

受影响的子条款:[dcl.init.agr]
更改:具有用户声明的构造函数从来都不是聚合
理由:删除可能应用的潜在错误倾向聚合初始化不管类的声明构造函数
对原始功能的影响:聚合初始化的有效C++2017代码具有用户声明构造函数的类型可能格式错误或具有本国际标准中的不同语义。

下面是我省略的例子。

该提案被接受并合并到C++20中,我们可以在这里找到包含这些更改的最新草案,我们可以看到对[dcl.init.agr]p1.1和[dcl.int.aggr]p17和C++17声明的更改。

所以这应该在C++20中得到修正。