【问题标题】:When is a private constructor not a private constructor?什么时候私有构造函数不是私有构造函数?
【发布时间】:2019-10-16 05:37:19
【问题描述】:

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

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
}    

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

【问题讨论】:

  • C c{}; 不是聚合初始化所以没有调用构造函数吗?
  • @NathanOliver 说了什么。您没有用户提供的构造函数,因此 C 是一个聚合。
  • @KerrekSB 同时,令我惊讶的是,明确声明 ctor 的用户并没有使该 ctor 由用户提供。
  • @Angew 这就是我们都在这里的原因:)
  • @Angew 如果是公开的=default ctor,那似乎更合理。但是私有的=default ctor 似乎是一件不容忽视的重要事情。更重要的是,class C { C(); } inline C::C()=default; 完全不同有点令人惊讶。

标签: c++ c++11 language-lawyer default-constructor aggregate-initialization


【解决方案1】:

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

【讨论】:

  • 实际上,一个小的标准缺陷:默认 ctor 是私有的这一事实在这种情况下实际上被忽略了。
  • @Yakk 我觉得没有资格判断这一点。不过,关于 ctor 不是由用户提供的措辞看起来非常刻意。
  • @Yakk:嗯,是的,也不是。如果该类有任何数据成员,您就有机会将它们设为私有。如果没有数据成员,这种情况很少会严重影响到任何人。
  • @KerrekSB 如果您尝试使用类一种“访问令牌”来控制例如谁可以根据谁可以创建类的对象来调用函数。
  • @Yakk 更有趣的是C{}即使构造函数是deleted也可以工作。
【解决方案2】:

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

来自[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;

【讨论】:

  • 我比 Angew 的回答更喜欢这个答案,但我认为它会受益于一开始最多两句话的总结。
【解决方案3】:

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])。

强调我的。现在要求没有 user-declared 构造函数,而过去是(正如两个用户在他们的答案中引用的那样,可以在历史上查看 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{} 恢复为调用默认构造函数,这又是格式良好的初始化。

【讨论】: