显式默认构造函数的目的

Posted

技术标签:

【中文标题】显式默认构造函数的目的【英文标题】:Purpose of Explicit Default Constructors 【发布时间】:2011-02-19 16:26:37 【问题描述】:

我最近注意到 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 是一个(可能是 cv 限定的)类类型(第 9 条),具有用户提供的构造函数(12.1),则调用 T 的默认构造函数(如果 T 没有可访问的默认构造函数);

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

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]。它需要几个转换构造函数,没有一个被标记为显式,但默认构造函数是。

【问题讨论】:

一发帖,我想我想出了一个解释。但我会等待我的怀疑得到证实,因为无论如何这似乎是一个有用的问题。 【参考方案1】:

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

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 各自的对象。值初始化不会进行重载解析舞蹈,因此不会在显式构造函数上失败。

【讨论】:

好的。那么std::function 的显式构造函数只是该版本草案的保留?正是这个解释,我在写完问题后终于想通了,但是提供的示例和std::function()都没有带可选参数,所以我并不完全相信。 这似乎有所改变,请参阅CWG 1518。最新版本的 g++ 和 clang++ 拒绝 function() 用于非默认的显式默认构造函数,即使在 C++11 模式下也是如此。 @VilleVoutilainen 这似乎是核心语言的另一个缺陷。为什么我们在值初始化时不考虑explicit默认构造函数用于= 触发的默认初始化,但是在寻找= foo 触发的构造函数时考虑explicit构造函数 值初始化?我对 LWG 问题解决方案的第一反应是“哦,不,这行不通,因为列表初始化会考虑 explicit 构造函数,并且只有在选择它们时,程序才是格式错误的。”。错误的 litb,C++ 在这里有另一个疣状的特殊情况,以引起额外的混乱:/ @VilleVoutilainen in coliru.stacked-crooked.com/a/25673a34a62d668e ,GCC 说这个电话是模棱两可的。但是,如果您删除 void f(B) 重载( coliru.stacked-crooked.com/a/7ee73c36f3d346e0 ),它会抱怨使用显式构造函数。看起来 GCC 也需要在这里进行一些改进(GCC6.3)来实现这个 IMO 疣。 @VilleVoutilainen 实际上,我认为我最初的反应似乎是正确的,GCC 的行为也是正确的,但是 LWG 的分辨率不正确。因为语言指定在重载解析期间, -> A 转换序列对于默认初始化没有特殊情况,而是使用一般的“考虑显式 ctor,如果选择显式则拒绝”: Quote: “如果初始化器列表没有元素且 T 有默认构造器,第一阶段被省略。在复制列表初始化中,如果选择显式构造器,则初始化是非良构的。"【参考方案2】:

除非另有明确说明,否则以下所有标准引用均指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) 如果没有找到可行的初始化列表构造函数,则再次执行重载决议,其中候选函数都是 T 类的构造函数和参数列表包括 初始化列表的元素。

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

copy-list-initialization 带有一个空的 braced-init-list 用于非聚合禁止使用显式默认构造函数;例如:

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:Foo & Bar 都是聚合 C++14:Foo & Bar 都是聚合 C++17:只有 Foo 是一个聚合(Bar 有一个 explicit 构造函数) C++20:FooBar 都不是聚合(两者都有用户声明的构造函数)

【讨论】:

以上是关于显式默认构造函数的目的的主要内容,如果未能解决你的问题,请参考以下文章

构造函数必须显式初始化没有默认构造函数的成员

显式默认构造函数

C++17 中的显式默认构造函数

为啥 std::in_place_t 的构造函数默认且显式?

使用枚举和模板参数正确定义显式默认构造函数

内联使用成员对象的非默认显式构造函数