删除了默认构造函数。仍然可以创建对象......有时

Deleted default constructor. Objects can still be created... sometimes

c++11统一初始化语法的幼稚、乐观和哦..如此错误的观点

我认为,由于 C++11 用户定义类型对象应该使用新的 {...} 语法而不是旧的 (...) 语法构造(除了为 [=18= 重载的构造函数) ] 和类似的参数(例如 std::vector:size ctor vs 1 elem init_list ctor))。

好处是:没有狭窄的隐式转换,最令人烦恼的解析、一致性(?)没有问题。我认为没有问题,因为我认为它们是相同的(给出的示例除外)。

但他们不是。

一个纯粹疯狂的故事

{}调用默认构造函数。

... 除非:

那么貌似是用值初始化对象吧?...即使对象删除了默认构造函数,{}也可以创建一个对象。这不是破坏了删除构造函数的全部目的吗?

...除非:

然后失败 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 f{3}; // OK

场景E

删除了默认构造函数,删除了 T 构造函数,T 数据成员

struct foo {
  int a;
  foo() = delete;
  foo(int) = delete;
};

foo f{}; // ERROR: missing initializer
foo f{3}; // OK

场景 F

删除了默认构造函数,in-class 数据成员初始值设定项

struct foo {
  int a = 3;
  foo() = delete;
};

/* Fa */ foo f{}; // ERROR: use of deleted function `foo::foo()`
/* Fb */ foo f{3}; // ERROR: no matching function to call `foo::foo(init list)`

当以这种方式查看事物时,很容易说对象的初始化方式完全混乱。

最大的区别在于foo的类型:是否为聚合类型。

It is an aggregate if it has:

  • no user-provided constructors (a deleted or defaulted function does not count as user-provided),
  • no private or protected non-static data members,
  • no brace-or-equal-initializers for non-static data members (since c++11 until (reverted in) c++14)
  • no base classes,
  • no virtual member functions.

所以:

  • 场景A B D E:foo是聚合
  • 在场景 C 中:foo 不是聚合
  • 场景 F:
    • 在 c++11 中它不是聚合。
    • 在 c++14 中它是一个集合。
    • g++ 尚未实现此功能,即使在 C++14 中仍将其视为非聚合。
      • 4.9 没有实现这个。
      • 5.2.0 确实
      • 5.2.1 ubuntu 没有(可能是回归)

The effects of list initialization of an object of type T are:

  • ...
  • If T is an aggregate type, aggregate initialization is performed. This takes care of scenarios A B D E (and F in C++14)
  • Otherwise the constructors of T are considered in two phases:
    • All constructors that take std::initializer_list ...
    • otherwise [...] all constructors of T participate in overload resolution [...] This takes care of C (and F in C++11)
  • ...

:

Aggregate initialization of an object of type T (scenarios A B D E (F c++14)):

  • Each non-static class member, in order appearance in the class definition, is copy-initialized from the corresponding clause of the initializer list. (array reference omitted)

TL;DR

所有这些规则仍然看起来非常复杂和令人头疼。我个人为自己过度简化了这一点(如果我因此搬起石头砸自己的脚,那就这样吧:我想我会在医院呆 2 天,而不是头痛几十天):

  • 对于聚合,每个数据成员都是从列表初始化器的元素初始化的
  • 否则调用构造函数

Doesn't this beat the whole purpose of a deleted constructor?

嗯,我不知道,但解决方案是使 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()`

此答案使用的信息来自:

非常感谢 @M.M 帮助更正和改进此 post。

让你烦恼的是聚合初始化

如您所说,使用列表初始化既有优点也有缺点。 (术语 "uniform initialization" 未被 C++ 标准使用)。

缺点之一是列表初始化对于聚合的行为与非聚合的行为不同。此外,聚合 的定义随每个标准略有不同。


聚合不是通过构造函数创建的。 (从技术上讲,它们实际上可能是,但这是一种很好的思考方式)。相反,在创建聚合时,会分配内存,然后根据列表初始化程序中的内容按顺序初始化每个成员。

非聚合是通过构造函数创建的,在这种情况下,列表初始化器的成员是构造函数参数。

上面其实有一个设计缺陷:如果我们有T t1; T t2{t1};,那么意图就是进行复制构造。但是,(在 C++14 之前)如果 T 是一个聚合,那么聚合初始化就会发生,并且 t2 的第一个成员被初始化为 t1.

此缺陷已在修改 C++14 的 defect report 中修复,因此从现在开始,在我们进行聚合初始化之前检查复制构造。


C++14 中聚合 的定义是:

An aggregate is an array or a class (Clause 9) with no user-provided constructors (12.1), no private or protected non-static data members (Clause 11), no base classes (Clause 10), and no virtual functions (10.3).

在 C++11 中,非静态成员的默认值意味着 class 不是聚合;然而,对于 C++14,这已经改变了。 User-provided 表示用户声明,但不是 = default= delete.


如果你想确保你的构造函数调用 永远不会 意外执行聚合初始化,那么你必须使用 ( ) 而不是 { },并且以其他方式避免 MVP。

这些围绕聚合初始化的情况对大多数人来说是违反直觉的,并且是提案 p1008: Prohibit aggregates with user-declared constructors 的主题,其中说:

C++ currently allows some types with user-declared constructors to be initialized via aggregate initialization, bypassing those constructors. The result is code that is surprising, confusing, and buggy. This paper proposes a fix that makes initialization semantics in C++ safer, more uniform, and easier to teach. We also discuss the breaking changes that this fix introduces

并介绍了一些示例,这些示例与您提供的案例很好地重叠:

struct X {
    X() = delete;
  };

 int main() {
    X x1;   // ill-formed - default c’tor is deleted
    X x2{}; // compiles!
}

Clearly, the intent of the deleted constructor is to prevent the user from initializing the class. However, contrary to intuition, this does not work: the user can still initialize X via aggregate initialization because this completely bypasses the constructors. The author could even explicitly delete all of default, copy, and move constructor, and still fail to prevent the client code from instantiating X via aggregate initialization as above. Most C++ developers are surprised by the current behaviour when shown this code The author of class X could alternatively consider making the default constructor private. But if this constructor is given a defaulted definition, this again does not prevent aggregate initialization (and thus, instantiation) of the class:

struct X {
  private:
    X() = default;
  };

int main() {
    X x1;     // ill-formed - default c’tor is private
    X x2{};  // compiles!
  }

Because of the current rules, aggregate initialization allows us to “default-construct” a class even if it is not, in fact, default-constructible:

 static_assert(!std::is_default_constructible_v<X>);

would pass for both definitions of X above.

...

提议的更改是:

Modify [dcl.init.aggr] paragraph 1 as follows:

An aggregate is an array or a class (Clause 12) with

  • no user-provided, explicit, u̲s̲e̲r̲-̲d̲e̲c̲l̲a̲r̲e̲d̲ or inherited constructors (15.1),

  • no private or protected non-static data members (Clause 14),

  • no virtual functions (13.3), and

  • no virtual, private, or protected base classes (13.1).

修改[dcl.init.aggr]第17段如下:

[Note: An aggregate array or an aggregate class may contain elements of a class >>type with a user-provided u̲s̲e̲r̲-̲d̲e̲c̲l̲a̲r̲e̲d̲ constructor (15.1). Initialization of >>these aggregate objects is described in 15.6.1. —end note]

将以下内容添加到附件 C 的 [diff.cpp17] 部分 C.5 C++ 和 ISO C++ 2017:

C.5.6 Clause 11: declarators [diff.cpp17.dcl.decl]

Affected subclause: [dcl.init.aggr]
Change: A class that has user-declared constructors is never an aggregate.
Rationale: Remove potentially error-prone aggregate initialization which may apply not withstanding the declared constructors of a class.
Effect on original feature: Valid C++ 2017 code that aggregate-initializes a type with a user-declared constructor may be ill-formed or have different semantics in this International Standard.

后面是我省略的例子。

提议是 accepted and merged into C++20 we can find the latest draft here which contains these changes and we can see the changes to [dcl.init.aggr]p1.1 and [dcl.init.aggr]p17 and C++17 declarations diff

所以这应该在 C++20 forward 中得到修复。