使用 { * this } 初始化类

19
有一位团队成员建议使用这样的初始化器:
return Demo{ *this };

优于:

return Demo(*this);

假设有一个简单的类如下所示:
class Demo {
public:
    int value1;
    Demo(){}
    Demo(Demo& demo) {
        this->value1 = demo.value1;
    }
    Demo Clone() {
        return Demo{ *this };
    }
};

我承认以前没有看到过{ *this }语法,并且找不到一个解释得足够好的参考资料,让我理解这两个选项有什么区别。这是一种性能上的优势、语法选择还是其他的东西呢?

所以他甚至没有告诉你为什么它更好吗? - David G
很遗憾,不行。代码已经提交并宣称更好了。 :) - WiredPrairie
没有性能优势。假设您没有初始化器列表构造函数,这符合相同的代码要求。 - RichardPlunkett
4
为什么不直接使用 return *this; - Aaron McDaid
@Simple:如果有人后来添加了一个initializer_list构造函数,那么这会改变Clone的行为吗?你的评论是在强调这一点,还是我漏掉了其他什么东西? - Rob
显示剩余2条评论
5个回答

16

你的同事在使用 "统一初始化" 方面有些不足,当类型已知时不需要使用类型名称。例如,在创建返回值时。 Clone 可以这样定义:

Demo Clone() {
    return {*this};
}

需要时,这将调用Demo的拷贝构造函数。你认为这是不是更好,取决于你自己。

GOTW 1中,Sutter提出了一项指南:

指南:倾向于使用初始化方式{ },例如vector v = {1, 2, 3, 4};或auto v = vector{1, 2, 3, 4};,因为它更一致、更正确,并且完全避免了必须了解旧式陷阱的问题。在单参数情况下,如果您只想看到=号,例如int i = 42;和auto x = anything;,省略花括号也可以。...

特别地,使用花括号可以避免以下混淆:

Demo d();      //function declaration, but looks like it might construct a Demo
Demo d{};      //constructs a Demo, as you'd expect

花括号语法将首先使用带有初始化器列表的构造函数(如果存在)。否则,它将使用普通构造函数。它还可以防止上面列出的令人困惑的解析机制的出现。

在使用复制初始化时也有不同的行为。使用标准方式
Demo d = x;
编译器有选项将x转换为Demo(如果需要),然后将转换后的右值移动/复制到w中。这类似于Demo d(Demo(x));,意思是调用了多个构造函数。
Demo d = {x};

这相当于 Demo d{x},并确保只调用一个构造函数。两个赋值都不能使用显式构造函数。

正如评论中提到的,有一些陷阱。对于那些接受initializer_list并且有“普通”构造函数的类可能会引起混淆。

vector<int> v{5};       // vector containing one element of '5'
vector<int> v(5);       // vector containing five elements.

3
然而,需要注意的是:vector<int> v{5};会创建一个只有一个元素且值为5的向量;而v(5)会创建包含五个默认为零的元素的向量。这可能会让一些人感到惊讶或觉得不一致。 - Aaron McDaid
8
我喜欢这种讽刺,为了避免陷阱而添加新的语法,结果又增加了更多的陷阱。 - Simple
聚合类也有一个陷阱:由于优先选择聚合初始化(如果适用),而不是选择构造函数,因此您无法通过大括号复制构造聚合类。即,如果Demo是一个聚合,则无法使用return {*this}来复制构造返回值,因为它将尝试从*this初始化第一个聚合成员。 - dyp
1
@简单来说,这是一个调用带有单个元素列表的函数和调用带有整数的函数之间的冲突。列表语法对于向量很有意义,但对于包含单个整数值的列表非常不兼容(请注意,此冲突不存在于非数字类型中)。这让我想起了Java中的ArrayList<>::remove,传入整数将删除索引位置上的元素,对于其他每种类型,它将删除该值。 - josefx

5

这只是另一种调用复制构造函数的语法(实际上,是调用以花括号中内容为参数的构造函数,在这种情况下,是调用复制构造函数)。

个人认为它比以前更糟糕,因为它只是依赖于C++11而已,并未带来好处。但是每个人的情况可能不同,你需要向你的同事询问。


4

我必须承认我以前从未见过这个。

维基百科关于C++11初始化列表的介绍如下(搜索“统一初始化”):

C++03在类型初始化方面存在许多问题。有几种初始化类型的方法,当它们互换时并不产生相同的结果。例如,传统的构造函数语法可能看起来像一个函数声明,必须采取措施确保编译器的最困惑的解析规则不会将其误认为是这样。只有聚合体和POD类型可以使用聚合初始化器进行初始化(使用SomeType var = {/stuff/};)。

然后,稍后他们有这个例子,

BasicStruct var1{5, 3.2}; // C type struct, containing only POD
AltStruct var2{2, 4.3};   // C++ class, with constructors, not 
                          // necessarily POD members

以下是解释:

var1的初始化行为就像聚合初始化一样。也就是说,对象的每个数据成员都将逐个从初始化列表中获取对应的值进行复制初始化。必要时会使用隐式类型转换。如果不存在转换或者只存在缩小转换,那么程序就是非法的。var2的初始化调用构造函数。

他们还提供了进一步的示例,针对提供初始化器列表构造函数的情况。

因此,仅基于上述内容:对于简单数据结构的情况,我不知道是否有任何优势。对于C++11类,使用{}语法可能有助于避免编译器认为你在声明一个函数的麻烦场景。也许这就是你同事所说的优势?


2

抱歉晚来参加此次讨论,但我想补充一些其他人未提到的初始化类型。

请考虑:

struct foo {
  foo(int) {}
};

foo f() {
  // Suppose we have either:
  //return 1;      // #1
  //return {1};    // #2
  //return foo(1); // #3
  //return foo{1}; // #4
}

然后,#1#3#4可能调用复制/移动构造函数(如果没有执行RVO),而#2不会调用复制/移动构造函数。
请注意,大多数流行的编译器确实执行RVO,因此,在实践中,上面的所有return语句都是等效的。但是,即使执行了RVO,对于#1#3#4,仍必须有一个可用的复制/移动构造函数(必须对f可访问并定义但未被删除),否则编译器/链接器将引发错误。
现在假设构造函数是显式的:
struct foo {
  explicit foo(int) {}
};

那么,#1#2不能编译,而#3#4可以编译。

最后,如果构造函数是显式的,并且没有可用的复制/移动构造函数:

struct foo {
  explicit foo(int) {}
  foo(const foo&) = delete;
};

所有的return语句都无法编译/链接。


1
这被称为列表初始化。其思想是在C++11中,您将拥有全面的统一初始化,并避免编译器可能认为您正在进行函数声明(也称为棘手的解析)的歧义。一个小例子:
vec3 GetValue()
{
  return {x, y, z}; // normally vec(x, y, z)
}

一个避免使用列表初始化的原因是,当您的类采用一个初始化器列表构造函数时,它可能会执行与您预期不同的操作。


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