删除的默认构造函数可能仍然微不足道

A deleted default constructor could still be trivial?

本文关键字:微不足道 默认 构造函数 删除      更新时间:2023-10-16

查看标准中普通默认构造函数的定义:

如果默认构造函数不是用户提供的,并且

如果:
  • 它的类没有虚函数(10.3)和虚基类(10.1),并且
  • 其类中没有非静态数据成员具有大括号或等于初始值设定项,并且
  • 其类的所有直接基类都有简单的默认构造函数,并且
  • 对于其类中属于类类型(或其数组)的所有非静态数据成员,每个此类都有一个简单的默认值 构造 函数。

否则,默认构造函数是非平凡的。

似乎默认构造函数的琐碎性的定义并不排除deleted默认构造函数的可能性:

struct A {
    int& a;  // the implicitly defaulted default constructor will be defined as deleted
};
struct B {
    B()=delete;  // explicitly deleted
};
int main() {
    static_assert(is_trivial<A>::value, "");
    static_assert(is_trivial<B>::value, "");
}

上面的代码运行没有任何断言失败。 该类型具有简单的默认构造函数,并且易于复制,因此它是一个"trivial class"

把这种类型做成"trivial class"不会带来麻烦吗? 例如,对于对象生存期、按字节复制等效性、goto语句余量等

编辑:以下goto允许示例无效。 感谢您@Casey的评论。 添加了另一个按字节复制等效的示例来替换此示例。

goto报表津贴为例,标准规定:

可以转移到一个块中,但不能以 通过初始化绕过声明。一个从87跳跃的程序 具有自动存储持续时间的变量不在 作用域到它在作用域中的点的格式不正确,除非 变量具有标量类型,具有简单默认值的类类型 构造函数和普通析构函数,一个符合 CV 标准的版本 这些类型或上述类型之一的数组,并声明 没有初始值设定项 (8.5)。

因此,对于以下代码:

class A {
    int& a;
public:
    A(int& aa): a{aa} {}
    A()=default;  // this is necessary otherwise no default constructor will be implicitly declared then the type will not be trivial
};
int i;
int main() {
    static_assert(is_trivial<A>::value, "");
    goto L;
    A a{i};
L:
    return 0;
}

根据规则,它的格式很好,因为它A有一个普通的默认构造函数和一个普通析构函数(断言传递 OK)。 相反,代码在 C++03 中的格式不正确(删除了仅 C++11 的语法,即A()=default;行),因为 A 不是 C++03 中的POD,而 C++03 允许goto仅交叉定义POD类型。

以逐字节复制等价为例,标准说:

对于任何可平凡复制的类型 T,如果指向 T 的两个指针指向 不同的 T 对象 obj1 和 obj2,其中 obj1 和 obj2 都不是 基类子对象,如果构成 obj1 的基础字节 (1.7) 是 复制到 OBJ2,41 OBJ2 随后应保持与 对象1.

因此,对平凡可复制类型的memcpy()是明确定义的:

class A {
    int& a;
public:
    A(int& aa): a{aa} {}
    A()=default;  // this is necessary otherwise no default constructor will be implicitly declared then the type will not be trivial
    void* addr() {return &a;}
};
int i = 0;
int j = 0;
int main() {
    static_assert(is_trivial<A>::value, "");
    A a{i};
    A b{j};
    cout << a.addr() << " " << b.addr() << "n";
    // a = b;  // this will be ill-formed because the implicitly defaulted copy assignment is defined as deleted
    memcpy(&a, &b, sizeof(A));  // this is well-defined because A is trivial
    cout << a.addr() << " " << b.addr() << "n";
}

它是根据规则明确定义的,因为它A是一个平凡的类型(断言通过 OK)。 结果表明,引用在不同时间引用不同的对象。 相反,代码在 C++03 中未定义(删除了仅 C++11 语法,即 A()=default; 行),因为A不是 C++03 中的POD,并且 C++03 仅允许POD类型的按字节复制等效性。

CWG 第 667 期通过一项更改解决了这个问题,该更改已合并到 N3225 附近的C++工作草案中。N3225 § 12.1 [class.ctor]/5 规定:

如果默认构造函数既不是用户提供的,也不是删除

的,并且如果:
  • 它的类没有虚函数(10.3)和虚基类(10.1),并且
  • 其类中没有非静态数据成员具有大括号或等于初始值设定项,并且
  • 其类的所有直接基类都有简单的默认构造函数,并且
  • 对于其类中属于类类型(或其数组)的所有非静态数据成员,每个此类都有一个简单的默认构造函数。

否则,默认构造函数是非平凡的。

这(显然)在 C++11 发布之前发生了变化。CWG DR 1135 的创建是为了解决芬兰国家机构对 C++11 候选草案的评论:

应该允许它在其第一个声明上显式默认非公共特殊成员函数。用户很可能希望默认受保护/私有构造函数和复制构造函数,而不必在类外部编写此类默认值。

此问题的解决方案从 12.1 中删除了"未删除"文本以及描述简单析构函数、简单复制/移动构造函数和简单复制/移动赋值运算符的部分。我认为这种变化的范围太广了,而且可能不是故意让你struct A变得微不足道。事实上,从表面上看,这个程序格式不正确是荒谬的:

int x = 42;
int y = 13;
A a_x{x};
A a_y{y};
a_x = a_y;

但这个程序不是,因为A是微不足道的可复制的(Clang同意,GCC不同意):

int x = 42;
int y = 13;
A a_x{x};
A a_y{y};
std::memcpy(&a_x, &a_y, sizeof(a_x));

CWG 第 1496 期"删除和丢失默认构造函数的琐碎性"的存在似乎表明委员会已经意识到了这个问题(或者至少是一个密切相关的问题):

根据 12.1 [class.ctor] 第 5 段,定义为已删除的默认构造函数是微不足道的。这意味着,根据9[类]第6段,这样的类可以是微不足道的。但是,如果该类没有默认构造函数,因为它具有用户声明的构造函数,则该类并非微不足道。由于这两种情况都阻止了类的默认构造,因此不清楚为什么这些情况之间存在琐碎性的差异。

虽然目前还没有解决方案。