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

97

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

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
}    
这让我感到非常惊讶、意外,而且明确是不希望的行为。为什么这是可以的呢?

27
C c{}; 不是聚合初始化吗,因此不会调用构造函数? - NathanOliver
5
@NathanOliver所说的没错。你没有提供用户构造函数,因此C是一个聚合体。 - Kerrek SB
5
同时,让我感到惊讶的是,明确声明构造函数的用户并不意味着该构造函数是用户自定义的。 - Angew is no longer proud of SO
2
如果它是一个公共的=default构造函数,那似乎更合理。 但私有的=default构造函数似乎是一个不应该被忽视的重要事情。 而且,class C { C(); } inline C::C()=default;这个也相当不同,有点令人惊讶。 - Yakk - Adam Nevraumont
1
@Barry,我确实认为这是一个缺陷。尽管“如果用户声明了函数并且在其第一次声明中没有显式地默认或删除,则A函数是由用户提供的”,但它并不是由用户提供的,因此您拥有一个聚合体。人们会期望private覆盖编译器生成的默认值为public - NathanOliver
显示剩余9条评论
3个回答

64
这个技巧在C++14 8.4.2/5 [dcl.fct.def.default]中:
“……如果一个函数是用户声明的并且在其第一次声明中没有显式地默认或删除, 那么它就是用户提供的函数。……”
这意味着C的默认构造函数实际上不是用户提供的,因为它在第一次声明时明确地被默认。因此,C没有用户提供的构造函数,因此符合8.5.1/1 [dcl.init.aggr]的聚合类型定义:
聚合体是一个没有用户提供的构造函数(12.1),没有非静态私有或保护数据成员(11节),没有基类(10节)和没有虚拟函数(10.3)的数组或类(9节)。 ”

13
实际上,存在一个小的标准缺陷:默认构造函数是私有的这一事实,在这种情况下实际上被忽略了。 - Yakk - Adam Nevraumont
2
@Yakk 我不太有资格评判这个。不过关于构造函数不是用户提供的措辞看起来非常有意思。 - Angew is no longer proud of SO
1
@Yakk:嗯,是的也不是。如果这个类有任何数据成员,你就有机会将它们设为私有。没有数据成员,很少有情况会严重影响任何人。 - Kerrek SB
2
如果你试图将类用作“访问令牌”,控制谁可以基于谁可以创建类的对象调用函数,则这很重要。 - Angew is no longer proud of SO
5
更有趣的是,即使构造函数被删除,C{}仍然可以正常工作。 - Barry
1
需要注意的是,C++20将修复此问题,将措辞更改为“没有用户声明的构造函数”。(https://timsong-cpp.github.io/cppwp/dcl.init.aggr#1) - Nicol Bolas

57

你并没有调用默认构造函数,而是在聚合类型上使用了聚合初始化。只要在第一次声明时将其默认化,聚合类型就可以拥有默认构造函数:

来自[dcl.init.aggr]/1

聚合是一个数组或类(第 [class] 条款),其中

  • 没有用户提供的构造函数(包括从基类继承的 [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;

2
我比Angew更喜欢这个答案,但我认为在开头加上最多两句话的摘要会更有益。 - PJTraill

8

Angew'sjaggedSpire's的回答非常好,适用于,以及。但是,在中,情况有所改变,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++11, C++14, 和C++17中历史性地查看)没有用户提供的构造函数。 C的默认构造函数是用户声明的,但不是用户提供的,因此在C++20中不再是聚合体。
这里有一个关于聚合变化的举例说明:
class A { protected: A() { }; };
struct B : A { B() = default; };
auto x = B{};

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

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

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接