什么时候私有构造函数不是私有构造函数?


88

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

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
}    

这使我感到非常惊奇,意外和明显不受欢迎的行为。为什么这样可以?


24
是不是C c{};聚合初始化,所以没有调用构造函数?
NathanOliver

5
@NathanOliver说了什么。您没有用户提供的构造函数,因此C没有集合。
Kerrek SB 2013年

5
@KerrekSB同时,令我感到非常惊讶的是,用户明确声明ctor不会使该ctor由用户提供。
Angew不再为

1
@Angew这就是为什么我们都在这里:)
Barry

2
@Angew如果它是一个公共=defaultctor,那似乎更合理。但是私有=defaultctor似乎是一件不容忽视的重要事情。而且,class C { C(); } inline C::C()=default;完全不同也有些令人惊讶。
Yakk-Adam Nevraumont

Answers:


58

诀窍在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 )。


13
实际上,这是一个小的标准缺陷:在这种情况下,默认ctor是私有的事实实际上被忽略了。
Yakk-Adam Nevraumont

2
@Y牛我没有资格去判断。但是,关于ctor不是由用户提供的措辞看起来非常有意思。
Angew不再为

1
@Yakk:是的,是的,不是的。如果班级有任何数据成员,您将有机会将其私有。没有数据成员,很少有情况会严重影响任何人。
Kerrek SB

2
@KerrekSB如果您要尝试使用该类的一种“访问令牌”,这很重要,例如,根据谁可以创建该类的对象来控制谁可以调用一个函数。
Angew不再为

5
@Yakk甚至更有趣的是,C{}即使构造函数为deleted ,它也可以工作。
巴里

55

您不是在调用默认构造函数,而是在聚合类型上使用聚合初始化。允许聚合类型具有默认的构造函数,只要它在首次声明的位置是默认的即可:

来自[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]),这可能意味着将它们定义为已删除。如果函数是由用户声明的,并且未在其第一个声明中显式默认或删除,则由用户提供。用户提供的显式默认函数(即,在其第一次声明后显式默认)在显式默认的点定义;如果将此类函数隐式定义为Delete,则程序格式错误。[注意:在首次声明函数后将其声明为默认函数可以提供有效的执行和简洁的定义,同时还可以为不断发展的代码库提供稳定的二进制接口。—尾注]

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

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

C 满足所有这些要求。

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

class C {
    C(){}
};
// --or--
class C {
    C();
};
inline C::C() = default;

2
我希望这个答案比Angew的答案要好一些,但是我认为从一开始最多以两句话作为总结将是有益的。
PJTraill

7

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{}恢复为调用默认构造函数,并且这也是格式正确的初始化。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.