显式默认构造函数的用途

时间:2010-05-14 19:16:10

标签: c++ default-constructor explicit explicit-constructor

我最近注意到C ++ 0x中的一个类需要一个显式的默认构造函数。但是,我没有想出一个可以隐式调用默认构造函数的场景。这似乎是一个相当无意义的说明者。我想也许它会禁止Class c;支持Class c = Class();,但似乎并非如此。

来自C ++ 0x FCD的一些相关引用,因为我更容易导航[类似文本存在于C ++ 03中,如果不在同一个地方]

  

12.3.1.3 [class.conv.ctor]

     

默认构造函数可以是显式构造函数;这样的构造函数将用于执行默认初始化或值初始化(8.5)。

它继续提供显式默认构造函数的示例,但它只是模仿我上面提供的示例。

  

8.5.6 [decl.init]

     

默认初始化T类型的对象意味着:

     

- 如果T是(可能是cv限定的)类类型(第9节),则调用T的默认构造函数(如果T没有可访问的默认构造函数,则初始化是错误的);

     

8.5.7 [decl.init]

     

对T类型的对象进行值初始化意味着:

     

- 如果T是具有用户提供的构造函数(12.1)的(可能是cv限定的)类类型(第9节),则调用T的默认构造函数(如果T没有,则初始化是错误的access default constructor);

在这两种情况下,标准都要求调用默认构造函数。但是如果默认构造函数是非显式的,那就会发生这种情况。为了完整起见:

  

8.5.11 [decl.init]

     

如果没有为对象指定初始化程序,则该对象是默认初始化的;

据我所知,这只是从没有数据的转换。这没有意义。我能想到的最好的是:

void function(Class c);
int main() {
  function(); //implicitly convert from no parameter to a single parameter
}

但显然这不是C ++处理默认参数的方式。还有什么能使explicit Class();的行为与Class();不同?

生成此问题的具体示例是std::function [20.8.14.2 func.wrap.func]。它需要几个转换构造函数,其中没有一个被标记为显式,但默认构造函数是。

2 个答案:

答案 0 :(得分:28)

这声明了一个显式的默认构造函数:

struct A {
  explicit A(int a1 = 0);
};

A a = 0; /* not allowed */
A b; /* allowed */
A c(0); /* allowed */

如果没有参数,如下例所示,explicit是多余的。

struct A {
  /* explicit is redundant. */
  explicit A();
};

在一些C ++ 0x草案中(我相信它是n3035),它在以下方面有所不同:

A a = {}; /* error! */
A b{}; /* alright */

void function(A a);
void f() { function({}); /* error! */ }

但是在FCD中,他们changed this(虽然,我怀疑他们没有考虑到这个特殊原因)因为所有三种情况 value-initialize 各自的对象。值初始化不会执行重载决策,因此在显式构造函数上不会失败。

答案 1 :(得分:1)

除非另有明确说明,否则以下所有标准参考文献均指N4659: March 2017 post-Kona working draft/C++17 DIS


(此答案专门针对没有参数的显式默认构造函数)


案例1 [ C ++ 11到C ++ 20 ]:非聚合的空{}复制列表初始化禁止使用显式的默认构造函数

[over.match.list]/1的约束[强调我的]:

当非聚合类类型为T的对象被列表初始化时,例如 [dcl.init.list]指定执行重载解析 根据本节中的规则,重载分辨率选择 构造函数分为两个阶段:

  • (1.1)最初,候选函数是类T和参数列表的初始化器列表构造函数([dcl.init.list])。 由初始化程序列表作为单个参数组成。
  • (1.2)如果找不到可行的initializer-list构造函数,则再次执行重载解析,其中所有候选函数 类T的构造函数和参数列表包括 初始化程序列表的元素。

如果初始化列表中没有元素,并且T具有默认值 构造函数,省略第一阶段。 在 复制列表初始化,如果选择了explicit构造函数, [注意:这与其他方法不同 情况([over.match.ctor],[over.match.copy]),仅 转换构造函数被考虑用于复制初始化。这个 限制仅在此初始化是最终的一部分时适用 解决过载的结果。 — 尾注]

复制列表初始化(用于非聚集)的空括号初始化列表 {}禁止使用显式默认构造函数;例如:

struct Foo {
    virtual void notAnAggregate() const {};
    explicit Foo() {}
};

void foo(Foo) {}

int main() {
    Foo f1{};    // OK: direct-list-initialization

    // Error: converting to 'Foo' from initializer
    // list would use explicit constructor 'Foo::Foo()'
    Foo f2 = {};
    foo({});
}

尽管上面的标准引用是针对C ++ 17的,但这同样适用于C ++ 11,C ++ 14和C ++ 20。


情况#2 [仅适用于C ++ 17 ]:具有用户声明的构造函数并标记为explicit的类类型不是集合

[dcl.init.aggr]/1在C ++ 14和C ++ 17之间进行了一些更新,主要是通过允许聚合从基类公开派生而来,但有一些限制,但也禁止了explicit构造函数用于聚集[重点我的]:

聚集是具有

的数组或类
  • (1.1)没有用户提供的explicit 或继承的构造函数([class.ctor]),
  • (1.2)没有私有或受保护的非静态数据成员(条款[class.access]),
  • (1.3)没有虚拟功能,并且
  • (1.4)没有虚拟,私有或受保护的基类([class.mi])。

P1008R1禁止使用用户声明的构造函数的聚合)(已针对C ++ 20实现)以来,我们可能再也不会声明聚合的构造函数。但是,仅在C ++ 17中,我们就有一条特殊的规则,即是否将用户声明的(但不是用户提供的)构造函数标记为显式,从而决定类类型是否为聚合类型。例如。班级类型

struct Foo {
    Foo() = default;
};

struct Bar {
    explicit Bar() = default;
};

在C ++ 11到C ++ 20中汇总/未汇总如下:

  • C ++ 11:FooBar都是聚合
  • C ++ 14:FooBar都是聚合
  • C ++ 17:只有Foo是聚合(Bar具有explicit构造函数)
  • C ++ 20:FooBar都不是聚合(都具有用户声明的构造函数)