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

时间:2016-06-03 15:27:21

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

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

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 个答案:

答案 0 :(得分:56)

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

答案 1 :(得分:51)

您没有调用默认构造函数,而是在聚合类型上使用聚合初始化。允许聚合类型具有默认构造函数,只要它在默认情况下默认为:

来自[dcl.init.aggr]/1

  

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

     
      
  • 没有用户提供的构造函数([class.ctor])(包括那些从基类继承的([namespace.udecl]),
  •   
  • 没有私有或受保护的非静态数据成员(Clause [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 :(得分:4)

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++11C++14和{ {3}})没有用户提供的构造函数。 C的默认构造函数是用户声明的,而不是用户提供的,因此在C ++ 20中不再是聚合。


这是汇总更改的另一个说明性示例:

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

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

在C ++ 17中,由于C++17的影响,聚合被扩展为允许基类。 B是C ++ 17中的聚合,这意味着B{}是聚合初始化,必须初始化所有子对象-包括A子对象。但是,由于A的默认构造函数受到保护,因此我们无法访问它,因此此初始化格式错误。

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

答案 3 :(得分:-3)

之所以有效,是因为您在构造函数中调用了std::initializer_list<>。因此,无论您的类是否为聚合类型,还是在c++14或类似的项目中都是棘手的,都没有关系。您可以执行以下操作以在任何情况下都不创建类对象

class C {
    C() = delete;
    C(const C&) = delete;
    C(C&&) = delete;
    C& operator=(const C&) = delete;
    template<typename T>
    C(std::initializer_list<T>) = delete;

};

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