在Herb Sutter的书 Exceptional C ++(1999)中,他在第10项解决方案中有词语:
“异常不安全”和“糟糕的设计”齐头并进。如果一段代码不是异常安全的,那通常是可以的并且可以简单地修复。但是,如果一段代码由于其底层设计而不能被设置为异常安全,那几乎总是这是其设计不佳的信号。
示例1:具有两种不同职责的职能很难使例外安全。
示例2:以必须检查自我分配的方式编写的复制赋值运算符可能不是非常强的异常安全
“检查自我指派”一词是什么意思?
[查询]
Dave和AndreyT向我们展示了“检查自我分配”的含义。非常好。但问题还没有结束。为什么“检查自我分配”会伤害“异常安全”(根据Hurb Sutter的说法)?如果调用者尝试进行自我分配,那么“检查”就像没有发生任何分配一样。这真的很痛吗?
[备忘1]在Herb的书后面的第38项对象身份中,他解释了自我分配。
答案 0 :(得分:33)
在这种情况下,更重要的问题是“以必须检查自我指派的方式编写”是指。
这意味着设计良好的赋值运算符不应需要来检查自我赋值。将对象分配给自己应该可以正常工作(即具有“无所事事”的最终效果),而无需执行显式的自我分配检查。
例如,如果我想沿
行实现一个简单的数组类class array {
...
int *data;
size_t n;
};
并提出了赋值运算符的以下实现
array &array::operator =(const array &rhs)
{
delete[] data;
n = rhs.n;
data = new int[n];
std::copy_n(rhs.data, n, data);
return *this;
}
该实施将被视为“坏”,因为在自我分配的情况下它显然会失败。
为了“修复”它,可以添加一个明确的自我分配检查
array &array::operator =(const array &rhs)
{
if (&rhs != this)
{
delete[] data;
n = rhs.n;
data = new int[n];
std::copy_n(rhs.data, n, data);
}
return *this;
}
或遵循“无检查”方法
array &array::operator =(const array &rhs)
{
size_t new_n = rhs.n;
int *new_data = new int[new_n];
std::copy_n(rhs.data, new_n, new_data);
delete[] data;
n = new_n;
data = new_data;
return *this;
}
后一种方法在某种意义上更好,因为它可以在自我分配情况下正常工作,而无需对其进行显式检查。 (从异常安全的角度来看,这种实现仍然是完美的,它是为了说明处理自我分配的“已检查”和“无检查”方法之间的区别)。通过众所周知的复制和交换习惯,可以更优雅地编写后期无需检查的实现。
这并不意味着您应该避免明确检查自我分配。从性能的角度来看,这样的检查是有意义的:执行长序列操作只是为了最终“无所事事”。但是在设计良好的赋值运算符中,从正确性的角度来看,这些检查不是必需的。
答案 1 :(得分:3)
来自c ++核心指南
Foo& Foo::operator=(const Foo& a) // OK, but there is a cost
{
if (this == &a) return *this;
s = a.s;
i = a.i;
return *this;
}
这显然是安全且明显有效的。但是,如果我们每百万个作业进行一次自我分配会怎么样?这是大约一百万次冗余测试(但由于答案基本上是一样的,因此计算机的分支预测器基本上每次都会猜对了)。考虑:
Foo& Foo::operator=(const Foo& a) // simpler, and probably much better
{
s = a.s;
i = a.i;
return *this;
}
注意:对于指针指向动态内存的类,上面的代码仅适用于没有指针的类。请参考Ant的回答。
答案 2 :(得分:2)
MyClass& MyClass::operator=(const MyClass& other) // copy assignment operator
{
if(this != &other) // <-- self assignment check
{
// copy some stuff
}
return *this;
}
将对象分配给自身是一个错误,但它不应该在逻辑上导致您的类实例发生变化。如果你设法设计一个分配给它自己改变它的类,它的设计很差。
答案 3 :(得分:0)
检查自我分配的一般原因是因为您在复制新数据之前销毁自己的数据。此赋值运算符结构也不是非常安全的异常。
作为附录,确定自我分配根本不会使性能受益,因为比较必须每次都进行,但是自我分配极少,如果确实发生,这是程序中的逻辑错误(真)。这意味着在整个计划过程中,这只是浪费周期。