类成员初始化的首选方式是什么?

5
class A { public: int x[100]; };

声明 A a 不会初始化对象(可以看到字段 x 中的垃圾值)。以下代码将触发初始化:A a{} 或者 auto a = A() 或者 auto a = A{}

应该偏好于三个中的哪一个呢?

接下来,让我们将它作为另一个类的成员:

class B { public: A a; };
< p > B 的默认构造函数似乎已经处理了 a 的初始化。 然而,如果使用自定义构造函数,我必须自己处理它。 以下两个选项可行:

class B { public: A a;  B() : a() { } };

或者:

class B { public: A a{};  B() { } };

应该优先选择其中的哪一个?

请说明您关于初始化程序使用上下文的问题。您只对空初始化程序感兴趣吗?还是您正在寻找确定复杂对象更一般方法的方法? - wally
1个回答

3

初始化

class A { public: int x[100]; };

Declaring A a will not initialize the object (to be seen by garbage values in the field x).

正确的A a没有初始化器,不满足默认初始化的任何要求。


1) The following will trigger initialization:

A a{};

是的;

  • a{} 执行列表初始化,如果 {} 是空的,则变成值初始化,或者如果 A 是一个聚合体则可以是聚合初始化
  • 即使默认构造函数被删除,也可以工作。例如:A() = delete;(如果 'A' 仍然被认为是一个聚合体)
  • 将警告狭窄转换。

2) The following will trigger initialization:

auto a = A();

是的;

  • 这是复制初始化,其中使用直接初始化()构造prvalue临时对象,如果()为空,则使用值初始化
  • 无法进行聚合初始化
  • 然后使用prvalue临时对象对对象进行直接初始化
  • 复制省略可能被并且通常被用来优化掉复制,并在原地构造A
  • 跳过复制/移动构造函数的副作用是允许的。
  • 移动构造函数不能被删除。例如A(A&&) = delete;
  • 如果复制构造函数被删除,则移动构造函数必须存在。例如A(const A&) = delete; A(A&&) = default;
  • 不会警告收窄转换。

3) The following will trigger initialization:

auto a = A{}

是的;

  • 拷贝省略(copy elision) 可以并且通常被用来优化掉拷贝并直接在该地方构造出 A 对象。
  • 可以允许跳过拷贝/移动构造函数的副作用(side effects)。
  • 移动构造函数可能不会被删除。例如: A(A&&) = delete;
  • 如果复制构造函数被删除,则必须存在移动构造函数。例如: A(const A&) = delete; A(A&&) = default;
  • 将会警告缩小类型转换(narrowing conversion)。
  • 即使默认构造函数被删除,也可以使用。例如: A() = delete; (如果'A'仍然被认为是一个聚合对象)

这三种初始化方式中是否应该特别优先考虑哪一种?

显然你应该优先选择 A a{}


成员初始化(Member Initialization)

Next, let us make it a member of another class:

class B { public: A a; };

The default constructor of B appears to take care of initialization of a.

不,这是不正确的。

  • 'B'的隐式默认构造函数将调用A的默认构造函数,但不会初始化成员。没有直接或列表初始化将被触发。例如语句B b;将调用默认构造函数,但保留了A数组的不确定值。

1) However, if using a custom constructor, I have to take care of it. The following two options work:

class B { public: A a;  B() : a() { } };

这将起作用;

2) or:

class B { public: A a{};  B() { } };

这将起作用;

显然,您应该选择第二个选项。


个人而言,我喜欢到处使用大括号,在某些情况下使用auto和构造函数可能会将其误认为是std :: initializer_list 的情况除外:

class B { public: A a{}; };

一个std::vector的构造函数对于std::vector<int> v1(5,10)std::vector<int> v1{5,10}会有不同的行为。使用(5,10),你将得到5个元素,每个元素的值都是10;但是使用{5,10},你将得到两个元素,分别包含5和10,因为如果使用大括号,则std::initializer_list优先被采用。这在Scott Meyers的Effective Modern C++的第7条中解释得非常好。
特别针对成员初始化列表,可以考虑两种格式: 在成员初始化列表中,幸运的是,不存在最麻烦的解析风险。作为单独语句时,A a()将声明一个函数,而A a{}则很清晰。此外,列表初始化有防止缩小转换的好处。
因此,总之,这个问题的答案取决于你想要确保什么,这将确定你选择的形式。对于空初始化器,规则更宽松。

请您详细说明构造函数误将 a{} 中的 {} 当作 std::initializer_list 的后果可能会有哪些影响? - Lasse Kliemann
我的问题特别针对空括号。我在您指出的书中找到了答案:空括号将调用默认构造函数,而不是使用空列表的初始化器列表构造函数。因此,空的 (){} 应该表现完全相同。 - Lasse Kliemann
关于空括号的有趣观点。我已经更新了答案,包括一些额外的要点。 - wally
说到最令人烦恼的解析问题,除了初始化列表外,还有一个相关的观察:使用{}而不是(),可以避免这个问题,但这只在某些情况下才是必要的。问题似乎在于允许在定义对象时将参数列表(即使为空)写在正在定义的对象旁边的构造函数中,即A a()。为了纠正这个问题,使用A a{}。然而,以下写法也是可以的:A a = A()auto a = A()。现在越来越多的人似乎会写成auto a = A{}并说“这样”可以避免大部分令人烦恼的解析问题。这种说法可能会让人感到困惑,因为这里的{}并没有起到区别作用。 - Lasse Kliemann
@LasseKliemann 有时候表单的形式很重要。我已经进行了一些更新。 - wally

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接