96

假设我有一个类型,我想将其默认构造函数设为私有。我写了以下内容:

class C {
    C() = default;
};

int main() {
    C c;           // error: C::C() is private within this context (g++)
                   // error: calling a private constructor of class 'C' (clang++)
                   // error C2248: 'C::C' cannot access private member declared in class 'C' (MSVC)
    auto c2 = C(); // error: as above
}

伟大的。

但是后来,构造函数并没有我想象的那么私密:

class C {
    C() = default;
};

int main() {
    C c{};         // OK on all compilers
    auto c2 = C{}; // OK on all compilers
}    

这让我觉得非常令人惊讶、出乎意料和明显不受欢迎的行为。为什么这样可以?

4

3 回答 3

64

诀窍在 C++14 8.4.2/5 [dcl.fct.def.default] 中:

...如果函数是用户声明的,并且在其第一次声明时未明确默认或删除,则该函数是用户提供的。...

这意味着C' 的默认构造函数实际上不是用户提供的,因为它在其第一个声明中被明确默认。因此,C没有用户提供的构造函数,因此是每个 8.5.1/1 [dcl.init.aggr] 的聚合:

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

于 2016-06-03T15:35:43.890 回答
57

您没有调用默认构造函数,而是在聚合类型上使用聚合初始化。聚合类型可以有一个默认的构造函数,只要它在第一次声明的地方被默认:

来自[dcl.init.aggr]/1

聚合是一个数组或一个类(子句 [class])

  • 没有用户提供的构造函数([class.ctor])(包括那些从基类继承的([namespace.udecl])),
  • 没有私有或受保护的非静态数据成员(子句 [class.access]),
  • 没有虚函数([class.virtual]),并且
  • 没有虚拟、私有或受保护的基类([class.mi])。

并来自[dcl.fct.def.default]/5

显式默认函数和隐式声明函数统称为默认函数,实现应为它们提供隐式定义([class.ctor] [class.dtor],[class.copy]),这可能意味着将它们定义为已删除. 如果函数是用户声明的,并且在其第一次声明时没有显式默认或删除,则该函数是用户提供的。用户提供的显式默认函数(即,在其第一次声明后显式默认)在显式默认的位置定义;如果这样的函数被隐式定义为已删除,则程序格式错误。[ 注意:在第一次声明后将函数声明为默认函数可以提供高效的执行和简洁的定义,同时为不断发展的代码库提供稳定的二进制接口。——尾注]

因此,我们对聚合的要求是:

  • 没有非公开成员
  • 没有虚函数
  • 没有虚拟或非公共基类
  • 没有继承或以其他方式提供的用户提供的构造函数,这仅允许以下构造函数:
    • 隐式声明,或
    • 同时显式声明和定义为默认值。

C满足所有这些要求。

当然,您可以通过简单地提供一个空的默认构造函数,或者在声明构造函数后将其定义为默认值来摆脱这种错误的默认构造行为:

class C {
    C(){}
};
// --or--
class C {
    C();
};
inline C::C() = default;
于 2016-06-03T15:36:30.260 回答
8

AngewjaggedSpire 的答案非常好,适用于。和。和

但是,在中,情况发生了一些变化,OP 中的示例将不再编译:

class C {
    C() = default;
};

C p;          // always error
auto q = C(); // always error
C r{};        // ok on C++11 thru C++17, error on C++20
auto s = C{}; // ok on C++11 thru C++17, error on C++20

正如两个答案所指出的,后两个声明起作用的原因是因为C是聚合,这是聚合初始化。但是,由于P1008(使用与 OP 不太相似的激励示例),C++20 中聚合的定义从[dcl.init.aggr]/1更改为:

聚合是一个数组或一个类 ([class])

  • 没有用户声明或继承的构造函数([class.ctor]),
  • 没有私有或受保护的直接非静态数据成员([class.access]),
  • 没有虚函数([class.virtual]),并且
  • 没有虚拟、私有或受保护的基类([class.mi])。

强调我的。现在的要求是没有用户声明的构造函数,而过去(正如两个用户在他们的答案中引用的并且可以在历史上查看C++11C++14C++17)没有用户提供的构造函数. 的默认构造函数C是用户声明的,但不是用户提供的,因此在 C++20 中不再是聚合。


这是聚合更改的另一个说明性示例:

class A { protected: A() { }; };
struct B : A { B() = default; };
auto x = B{};

B在 C++11 或 C++14 中不是聚合,因为它有一个基类。因此,B{}只需调用默认构造函数(用户声明但非用户提供),该构造函数可以访问A受保护的默认构造函数。

在 C++17 中,作为P0017的结果,聚合被扩展以允许基类。B是 C++17 中的聚合,这意味着B{}聚合初始化必须初始化所有子对象 - 包括A子对象。但是因为A的默认构造函数是受保护的,我们无权访问它,所以这个初始化是非良构的。

在 C++20 中,由于B用户声明的构造函数,它再次不再是聚合,因此B{}恢复为调用默认构造函数,这又是格式良好的初始化。

于 2019-08-09T12:49:50.167 回答