显式默认构造函数的目的
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[强调我的]管理:
当非聚合类类型
(1.1) 最初,候选函数是类T
的对象被列表初始化时 [dcl.init.list] 指定执行重载决议 根据本节的规则,重载决议选择 构造函数分两个阶段: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:Foo
或 Bar
都不是聚合(两者都有用户声明的构造函数)
【讨论】:
以上是关于显式默认构造函数的目的的主要内容,如果未能解决你的问题,请参考以下文章