删除了默认构造函数。仍然可以创建对象...有时
Posted
技术标签:
【中文标题】删除了默认构造函数。仍然可以创建对象...有时【英文标题】:Deleted default constructor. Objects can still be created... sometimes 【发布时间】:2015-11-29 21:22:00 【问题描述】:对c++11统一初始化语法的天真、乐观和哦..这么错误的看法
我认为既然 C++11 用户定义类型对象应该使用新的 ...
语法而不是旧的 (...)
语法来构造(除了为 std::initializer_list
和类似参数重载的构造函数(例如 @987654325 @: size ctor vs 1 elem init_list ctor))。
好处是:没有狭窄的隐式转换,最麻烦的解析没有问题,一致性(?)。我认为没有问题,因为我认为它们是相同的(除了给出的示例)。
但他们不是。
纯粹的疯狂故事
调用默认构造函数。
... 除非:
默认构造函数被删除并且 没有定义其他构造函数。那么看起来是不是用value来初始化对象?...即使对象已经删除了默认构造函数,也可以创建对象。这难道不是违背了已删除构造函数的全部目的吗?
...除非:
该对象有一个已删除的默认构造函数,并且 定义了其他构造函数。然后它以call to deleted constructor
失败。
...除非:
对象有一个已删除的构造函数,并且 没有定义其他构造函数并且 至少是一个非静态数据成员。然后它会因缺少字段初始值设定项而失败。
但是你可以使用value
来构造对象。
好吧,也许这和第一个异常一样(值初始化对象)
...除非:
该类有一个已删除的构造函数 并且至少有一个数据成员在类内默认初始化。那么 和
value
都不能创建对象。
我确定我错过了一些。具有讽刺意味的是,它被称为uniform 初始化语法。我再说一遍:UNIFORM初始化语法。
这是什么疯子?
场景 A
已删除默认构造函数:
struct foo
foo() = delete;
;
// All bellow OK (no errors, no warnings)
foo f = foo;
foo f = ;
foo f; // will use only this from now on.
场景 B
删除默认构造函数,删除其他构造函数
struct foo
foo() = delete;
foo(int) = delete;
;
foo f; // OK
场景 C
删除默认构造函数,定义其他构造函数
struct foo
foo() = delete;
foo(int) ;
;
foo f; // error call to deleted constructor
场景 D
已删除默认构造函数,未定义其他构造函数,数据成员
struct foo
int a;
foo() = delete;
;
foo f; // error use of deleted function foo::foo()
foo f3; // OK
场景 E
删除默认构造函数,删除T构造函数,T数据成员
struct foo
int a;
foo() = delete;
foo(int) = delete;
;
foo f; // ERROR: missing initializer
foo f3; // OK
场景 F
删除默认构造函数,类内数据成员初始化器
struct foo
int a = 3;
foo() = delete;
;
/* Fa */ foo f; // ERROR: use of deleted function `foo::foo()`
/* Fb */ foo f3; // ERROR: no matching function to call `foo::foo(init list)`
【问题讨论】:
这个***.com/questions/23882409/… 回答了一半的问题。最重要的一个,但仍然没有回答类内数据成员初始化和非默认构造函数会发生什么。 对不起,我太仓促了。在这里,聚合初始化的执行正是因为构造函数被定义为已删除(在其第一次声明时)。 这不是现代 C++ 中疯狂的单一案例。多年来,我听说“C++ 很愚蠢,因为static
根据上下文的不同意味着非常不同的东西”(实际上只有两个非常不同的含义,并且在明显不同的上下文中)。然后decltype
被发明了两个微妙不同的含义和非常微妙的不同用途:identifier
与:(identifier)
"没有狭义的隐式转换" 在一个特殊情况下禁止完全有效和有用的转换是件好事吗?
@curiousguy 我不明白你的意思
【参考方案1】:
当以这种方式看待事物时,很容易说对象的初始化方式完全混乱。
最大的区别在于foo
的类型:是否为聚合类型。
如果有,则为聚合:
没有用户提供的构造函数(已删除或默认的函数不算作用户提供), 没有私有或受保护的非静态数据成员, 非静态数据成员没有大括号或等号初始化器(从 c++11 到(恢复到)c++14) 没有基类, 没有虚拟成员函数。
所以:
在场景 A B D E 中:foo
是一个聚合
在场景 C 中:foo
不是聚合
场景F:
在 c++11 中它不是聚合。
在 c++14 中它是一个聚合。
g++ 没有实现这一点,即使在 C++14 中仍然将其视为非聚合。
4.9
没有实现这一点。
5.2.0
会
5.2.1 ubuntu
没有(可能是回归)
T 类型对象的列表初始化的效果是:
... 如果 T 是聚合类型,则执行聚合初始化。这会处理场景 A B D E(和 C++14 中的 F) 否则 T 的构造函数将分两个阶段考虑: 所有采用 std::initializer_list 的构造函数... 否则 [...] T 的所有构造函数都参与重载决议 [...] 这会处理 C(和 C++11 中的 F) ...
:
T类型对象的聚合初始化(场景A B D E (F c++14)):
每个非静态类成员,为了出现在类定义中,都是从对应的子句复制初始化的 初始化列表。 (省略数组引用)
TL;DR
所有这些规则看起来仍然非常复杂且令人头疼。我个人为自己过度简化了这一点(如果我因此在脚上开枪,那就这样吧:我想我会在医院呆两天而不是头痛十几天):
对于聚合,每个数据成员都是从列表初始化程序的元素初始化的 其他调用构造函数这难道不符合删除构造函数的全部目的吗?
好吧,我不知道,但解决方案是使 foo
不是聚合。不增加开销并且不改变对象使用的语法的最通用形式是使其继承自空结构:
struct dummy_t ;
struct foo : dummy_t
foo() = delete;
;
foo f; // ERROR call to deleted constructor
在某些情况下(我猜根本没有非静态成员),另一种方法是删除析构函数(这将使对象在任何上下文中都不可实例化):
struct foo
~foo() = delete;
;
foo f; // ERROR use of deleted function `foo::~foo()`
此答案使用从以下网站收集的信息:
C++14 value-initialization with deleted constructor
What are Aggregates and PODs and how/why are they special?
List initialization
Aggregate initialization Direct initialization非常感谢@M.M,他帮助纠正和改进了这篇文章。
【讨论】:
能否请熟悉该标准的人仔细检查我的答案,以确保我没有犯错。我仍然没有完全掌握复杂的初始化规则。非常感谢。 “非静态数据成员没有大括号或等式初始化器(c++11 起)”——这在 C++14 中被恢复 当你真正的意思是“非聚合初始化”时,你写的是“直接初始化”。术语直接初始化指的是Ta,b,c
(和其他情况),无论T
是否是一个聚合。所有初始化都是直接初始化或复制初始化(还有可用的子分类)。
我实际上关注了en.cppreference.com/w/cpp/language/list_initialization。我知道这不是规范参考
C++17 将允许聚合具有公共基础,因此您需要将 dummy_t
设为私有(或受保护,但我不明白这一点)基础才能工作.【参考方案2】:
搞砸你的是聚合初始化。
正如您所说,使用列表初始化有利有弊。 (C++ 标准不使用术语“统一初始化”)。
其中一个缺点是聚合与非聚合的列表初始化行为不同。此外,aggregate 的定义会随着每个标准的不同而略有不同。
聚合不是通过构造函数创建的。 (从技术上讲,它们实际上可能是,但这是一种很好的思考方式)。相反,在创建聚合时,会分配内存,然后根据列表初始化程序中的内容按顺序初始化每个成员。
非聚合是通过构造函数创建的,在这种情况下,列表初始值设定项的成员是构造函数参数。
上面其实有一个设计缺陷:如果我们有T t1; T t2t1;
,那么意图是执行复制构造。但是,(在 C++14 之前)如果 T
是一个聚合,则改为进行聚合初始化,并且 t2
的第一个成员使用 t1
进行初始化。
此缺陷已在修改 C++14 的 defect report 中得到修复,因此从现在开始,在进行聚合初始化之前检查复制构造。
聚合从C++14的定义是:
聚合是一个数组或一个类(第 9 条),没有用户提供的构造函数(12.1),没有私有或受保护的非静态数据成员(第 11 条),没有基类(第 10 条),也没有虚拟函数 (10.3)。
在 C++11 中,非静态成员的默认值意味着类不是聚合;但是对于 C++14,情况发生了变化。 User-provided 表示用户声明,但不是= default
或= delete
。
如果你想确保你的构造函数调用从不意外执行聚合初始化,那么你必须使用( )
而不是
,并以其他方式避免MVP。
【讨论】:
恕我直言,这方面的标准需要改进。如果类可以正式标记为aggregate
或非聚合,或者如果“聚合构造函数”被正式标记,那就太好了。
或者可能完全摆脱聚合类,即根据非聚合规则处理所有类。
我认为缺陷报告是针对 C++11 提交的,因此可以在 C++14 中修复。
@5gon12eder 见CWG 1467。 C++14 发布时没有修复;然后它在 2014 年 11 月被接受为缺陷。我不确定这是否意味着它被认为适用于 C++11(可能没有实际意义,因为正式 C++14 完全取代了 C++11 ,尽管编译器必须决定在-std=c++11
模式下做什么)
"如果我们有T t2t1;
,那么目的是执行复制构造。" — 除非T
是std::vector<boost::any>
,在这种情况下,意图是用一个初始化列表初始化t2
,其单个元素是t1
(的副本)。这是Core Issue 2137 的主题,如果幸运的话,它可能会在 C++17 中重新修复。 (它在 C++11 中很好,然后在 C++14 中被破坏。)如果你真的必须有复制构造,那么它的语法是 auto t2 = t1;
。【参考方案3】:
这些关于聚合初始化的案例对大多数人来说是违反直觉的,并且是 p1008: Prohibit aggregates with user-declared constructors 提案的主题,该提案说:
C++ 目前允许通过聚合初始化某些具有用户声明的构造函数的类型 初始化,绕过那些构造函数。结果是令人惊讶、令人困惑的代码 越野车。本文提出了一个修复,使 C++ 中的初始化语义更安全、更统一、 并且更容易教。我们还讨论了此修复引入的重大更改
并介绍一些示例,这些示例与您提供的案例很好地重叠:
struct X X() = delete; ; int main() X x1; // ill-formed - default c’tor is deleted X x2; // compiles!
很明显,删除构造函数的目的是防止用户初始化类。然而,与直觉相反,这是行不通的:用户仍然可以初始化 X 通过聚合初始化,因为这完全绕过了构造函数。作者甚至可以显式删除所有默认、复制和移动构造函数,但仍然无法阻止客户端代码通过上述聚合初始化来实例化 X。大多数 C++ 开发人员对 显示此代码时的当前行为 X 类的作者也可以考虑制作默认构造函数 私人的。但如果 这个构造函数有一个默认定义,这也不会阻止类的聚合初始化(因此,实例化):
struct X private: X() = default; ; int main() X x1; // ill-formed - default c’tor is private X x2; // compiles!
由于当前的规则,聚合初始化允许我们“默认构造”一个类,即使它实际上不是可默认构造的:
static_assert(!std::is_default_constructible_v<X>);
上述 X 的两个定义都会通过。
...
建议的更改是:
修改[dcl.init.aggr]第1段如下:
聚合是一个数组或一个类(第 12 条),带有
没有用户提供的,明确的,u̲s̲e̲r̲-̲d̲e̲c̲l̲a̲r̲e̲d̲或继承 构造函数(15.1),没有私有或受保护的非静态数据成员(第 14 条),
没有虚函数 (13.3),并且
没有虚拟、私有或受保护的基类 (13.1)。
将[dcl.init.aggr]第17段修改如下:
[注意:聚合数组或聚合类可能包含具有
用户提供u̲s̲e̲r̲-̲d̲e̲c̲l̲a̲r̲e̲d̲的类>>类型的元素> 构造函数 (15.1)。 >> 这些聚合对象的初始化在 15.6.1 中描述。 ——尾注]将以下内容添加到附件 C 的 C.5 C++ 和 ISO C++ 2017 部分的 [diff.cpp17] 中:
C.5.6 第 11 条:声明符 [diff.cpp17.dcl.decl]
受影响的子条款:[dcl.init.aggr]更改:具有 用户声明的构造函数永远不是聚合。基本原理:删除 可能适用的可能容易出错的聚合初始化 不承受类的声明构造函数。对原始功能的影响:有效的 C++ 2017 代码聚合初始化 具有用户声明的构造函数的类型可能格式错误或具有 本国际标准中的不同语义。
后面是我省略的例子。
提案是accepted and merged into C++20,我们可以找到包含这些更改的latest draft here,我们可以看到[dcl.init.aggr]p1.1、[dcl.init.aggr]p17和C++17 declarations diff的更改。
所以这应该在 C++20 中修复。
【讨论】:
它似乎在 c++20 中已修复:godbolt.org/z/Majaf6nTb以上是关于删除了默认构造函数。仍然可以创建对象...有时的主要内容,如果未能解决你的问题,请参考以下文章