38

在 Herb Sutter 的著作Exceptional C++ (1999)中,他在第 10 项的解决方案中有一句话:

“异常不安全”和“糟糕的设计”齐头并进。如果一段代码不是异常安全的,那通常没关系,可以简单地修复。但是,如果一段代码由于其底层设计而不能成为异常安全的,那几乎总是表明其设计不佳。

示例 1:具有两种不同职责的函数很难使异常安全。

示例 2:以必须检查自赋值的方式编写的复制赋值运算符可能也不是强异常安全的

他所说的“检查自我分配”是什么意思?

[询问]

Dave 和 AndreyT 准确地向我们展示了“检查自我分配”的含义。那挺好的。但问题还没有结束。为什么“检查自我分配”会损害“异常安全”(根据 Hurb Sutter 的说法)?如果调用者尝试进行自分配,则该“检查”就像没有发生分配一样工作。真的很痛吗?

[备忘录 1] 在 Herb 书中后面的第 38 项Object Identity中,他解释了自我分配。

4

4 回答 4

42

在这种情况下,一个更重要的问题是:


  • “这是什么意思,当你以某种方式编写函数时,需要你检查自赋值???”

回答我的反问:这意味着设计良好的赋值运算符不需要检查自赋值。将一个对象分配给它自己应该可以正常工作(即具有“什么都不做”的最终效果),而不需要对自分配进行显式检查。

例如,如果我想按照以下方式实现一个简单的数组类

class array {
    // code...
 
    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;
}




该实现将被认为是“糟糕的”,因为它在自我分配的情况下显然会失败。

为了“解决”这个问题,开发人员有两个选择;

  1. 他或她可以添加明确的自我分配检查
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;
}

  1. 或者您可以遵循“无检查”方法:
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;
}





后一种方法在某种意义上更好,因为它可以在自我分配情况下正常工作,而无需进行明确的检查。从“安全角度”来看,这个实现远非完美,这里是为了说明处理自分配的“检查”和“无检查”方法之间的区别。后面的无检查实现可以通过众所周知的复制和交换习语来更优雅地编写。

这并不意味着您应该避免对自分配进行显式检查。从性能的角度来看,这样的检查确实是有意义的:执行一长串操作只是为了最终“什么都不做”是没有意义的。但在设计良好的赋值运算符中,从正确性的角度来看,这样的检查是不必要的。

于 2012-08-18T02:00:36.873 回答
10

来自 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;
}

注意:上面的代码只适用于没有指针的类,因为有指针的类指向动态内存。请参考蚂蚁的回答。

于 2017-05-04T16:33:37.730 回答
3
MyClass& MyClass::operator=(const MyClass& other)  // copy assignment operator
{
    if(this != &other) // <-- self assignment check
    {
        // copy some stuff
    }

    return *this;
}

将对象分配给自身是一个错误,但它不应该在逻辑上导致您的类实例发生更改。如果您设法设计一个分配给自身的类会改变它,那么它的设计就很糟糕。

于 2012-08-18T01:51:10.723 回答
1

检查自分配的一般原因是因为您在复制新数据之前破坏了自己的数据。这种赋值运算符结构也不是强异常安全的。

作为附录,已确定自分配根本不会提高性能,因为每次都必须运行比较,但自分配极为罕见,如果确实发生,这是您的程序中的逻辑错误(真的) . 这意味着在整个程序过程中,这只是对周期的浪费。

于 2012-08-18T03:04:33.247 回答