使用 { * this } 初始化类

Initializing class using { * this }

本文关键字:初始化 this 使用      更新时间:2023-10-16

一位团队成员建议使用这样的初始化器:

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 }语法,并且找不到一个足够好的参考来解释它,以至于我理解这两个选项的不同之处。是否有性能优势、语法选择或更多?

您的同事缺少"统一初始化"的技巧,当知道类型名称时,不需要类型名称。 例如,在创建返回值时。Clone可以定义为:

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

这将根据需要调用Demo复制构造函数。 你是否认为这更好,取决于你。

在GOTW 1中,萨特作为准则指出:

指南:更喜欢使用 { } 进行初始化,例如向量 v = { 1, 2, 3, 4 };

或 auto v = vector{ 1, 2, 3, 4 };,因为它更一致、更正确,并且完全不必了解旧式陷阱。在单参数情况下,您只希望看到 = 符号,例如 int i = 42;和自动 x = 任何东西;省略大括号很好。。

特别是,使用大括号可以避免与以下情况混淆:

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,然后将转换后的 r 值移动/复制到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.

这只是调用复制构造函数的另一种语法(实际上,用于调用将大括号中的内容作为参数的构造函数,在本例中为复制构造函数)。

就个人而言,我会说它比以前更糟,因为它做了同样的事情......它只是依赖于C++ 11。因此,它增加了没有好处的依赖关系。但您的里程可能会有所不同。你将不得不问你的同事。

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

维基百科对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 类,使用 {} 语法可能有助于避免编译器认为您正在声明函数的那些讨厌的情况。也许这就是你的同事所指的优势?

很抱歉来晚了这个讨论,但我想补充一些关于其他人没有提到的不同类型的初始化的观点。

考虑:

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,复制/移动构造函数也必须f可用于#1#3#4否则编译器/链接器将引发错误。

现在假设构造函数是显式的:

struct foo {
explicit foo(int) {}
};

然后

#1#2不编译,而#3#4编译。

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

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

没有return语句编译/链接。

这称为list-initialization。这个想法是,在 C++11 中,您将全面进行统一的初始化,并避免编译器可能认为您可能正在进行函数声明(也称为烦人的解析)的歧义。一个小例子:

vec3 GetValue()
{
return {x, y, z}; // normally vec(x, y, z)
}

您希望避免列表初始化的一个原因是,您的类采用初始值设定项列表构造函数,该构造函数执行与预期不同的操作。