使用*this来初始化类是否会有任何后果?

30
在我编写的小游戏中,我有一个名为Weapon 的类,其中包含两个构造函数。一个构造函数需要输入一些参数以生成自定义武器,另一个构造函数则会获得默认武器(CHAIN_GUN)。
Weapon::Weapon (void) {
    // Standard weapon
    *this = getWeapon(CHAIN_GUN);
    return;
}
问题:使用 *thisoperator= 来初始化一个类会有负面影响吗?

很常见看到复制构造函数以类似的方式实现。 - Kik
5个回答

33
假设有人让你画一幅画...你会:
首先画出你的默认(第一个)(那个你喜欢的熟悉的笑脸),
然后画出那个人要求的东西(第二个),
只是为了在包含你的默认设置的画布上再次画出同样的东西,
然后烧掉第二幅画?
本篇文章将尝试解释为什么这个比喻很相关。

为什么这是个糟糕的想法?

我从未见过使用赋值运算符实现默认构造函数,老实说,我不建议这样做,在代码审核期间也不支持。

这种代码的主要问题在于,根据定义,我们正在构造两个对象(而不是一个),并调用成员函数,这意味着我们需要构造所有成员两次,并稍后通过调用赋值运算符来复制/移动初始化所有成员。

请求构造1个对象时,我们构造2个对象,然后将值从第二个对象复制到第一个对象并丢弃第二个对象,这是不直观的。

结论不要这样做

注意:如果Weapon有基类,则情况会更糟)

(注意:另一个潜在的危险是工厂函数意外使用默认构造函数,在编译期间未被捕获,导致无限递归,如@Ratchet Freat所指出)

建议解决方案

在您的特定情况下,最好在构造函数中使用默认参数,如下例所示。

class Weapon {
public:
  Weapon(WeaponType w_type = CHAIN_GUN);
  ...
}

Weapon w1;             // w_type = CHAIN_GUN
Weapon w2 (KNOWLEDGE); // the most powerful weapon

(注意:以上的另一种方法是使用C++11中可用的委托构造函数)


2
存在一个风险,即工厂使用默认构造函数(现在或将来在无知的更改中),导致无限递归。 - ratchet freak
2
通常在单参数构造函数前加上 explicit 可以避免隐式类型转换,这是一个好的习惯。 - Dave

16

使用赋值运算符来实现构造函数很少是一个好主意。在你的情况下,例如,你可以只使用默认参数:

Weapon::Weapon(GunType g = CHAIN_GUN)
: // Initialize members based on g
{
}
在其他情况下,您可以使用委托构造函数(在C++11或更高版本中):
Weapon::Weapon(GunType g)
: // Initialize members based on g
{
}

Weapon::Weapon()
: Weapon(CHAIN_GUN) // Delegate to other constructor
{
}

2
请注意,委托构造函数需要C++11或更高版本。 - Remy Lebeau
这是我个人认为的最佳解决方案;如果你的编译器不支持委托构造函数,则创建一个名为 construct_me(GunType g) 的公共函数,在每个构造函数中调用它。 - M.M

7
需要翻译的内容:

需要记住的一件事情是,如果operator= - 或任何它调用的函数 - 是virtual,那么派生类的版本不会被调用。这可能会导致未初始化的字段和后续的未定义行为,但这完全取决于你的数据成员。

更一般而言,如果基类和数据成员具有构造函数或出现在初始值列表中(或在C++11中出现在类声明中进行赋值),则保证它们已被初始化 - 因此除了上述的virtual问题外,operator=通常可以在没有未定义行为的情况下工作。

如果在调用operator=()之前已对基类或成员进行了初始化,则无论如何都会覆盖初始值,优化器可能能够删除第一个初始化。例如:

std::string s_;
Q* p_;
int i_;

X(const X& rhs)
  : p_(nullptr)  // have to initialise as operator= will delete
{
    // s_ is default initialised then assigned - optimiser may help
    // i_ not initialised but if operator= sets without reading, all's good
    *this = rhs;
}

正如您所看到的,这种方法有点容易出错,即使您正确地编写了代码,稍后更新operator =的人也可能没有检查构造函数是否被滥用....
如果getWeapon()使用原型或享元模式并尝试复制构造它返回的Weapon,则可能导致无限递归,从而导致堆栈溢出。
退一步说,为什么要以那种形式存在getWeapon(CHAIN_GUN);的问题。如果我们需要一个基于武器类型创建武器的函数,那么Weapon(Weapon_Type);构造函数似乎是一个合理的选择。话虽如此,有时候会出现罕见但大量的边缘情况,其中getWeapon可能会返回除Weapon对象之外的其他东西,但仍然可以分配给Weapon,或者由于构建/部署原因而保持分离....

1
有时会出现罕见但很多的边缘情况。不错。 - Cthulhu
1
@Cthulhu:是啊,我的措辞不太好——罕见是指很少遇到的,而且丰富多样是指许多不同的习语/模式/设计。:D - Tony Delroy

3
如果您已定义了一个非拷贝的 = 赋值运算符,以便让 Weapon 在构造后更改其类型,则使用赋值实现构造函数就可以很好地解决问题,并且是集中初始化代码的一种好方法。 但是,如果一个 Weapon 在构造后不打算更改其类型,则没有必要创建非拷贝的 = 赋值运算符,更不用说用于初始化了。

3

我肯定是的。

您已经在“getWeapon”函数内创建了对象,然后将其复制,这可能是一项长时间操作。因此,至少您必须尝试移动语义。

但是。 如果在“getWeapon”内部调用构造函数(而且您确实这样做了,某种方式下,“getWeapon”必须创建要返回的类),则会创建非常不清晰的架构,其中一个构造函数调用调用另一个构造函数的函数。

我认为您必须将参数初始化分离到私有函数中,并从您想要的构造函数中调用它们。


嗯,我相信“我相信”的答案不一定是一个好答案。 - Sebastian Mach
1
我的意思是“依我之见”或“我确定”。如果这与“我确定”不同,那么这是我的语言问题,如果在我的“脑海中”是“我确定”:-) 我认为任何人都可能犯错,也许会有这样的情况和代码,这些东西可能很有用。这就是为什么我首先提出了移动语义。所以,人们寻求建议,我从自己的经验中给出了建议。我不确定这是最好的经验,这就是为什么我加入了“依我之见”的变体。 - Arkady

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