原文链接:

OOP54-CPP. Gracefully handle self-copy assignment


自拷贝赋值会可能出现在复杂多样的场景中,不过基本上,所有自拷贝赋值涉及如下情况。

1
2
3
4
5
6
7
8
#include <utility>

struct S { /* ... */ }

void f() {
S s;
s = s; // Self-copy assignment
}

用户提供的拷贝操作必须正确处理自拷贝赋值。

C++标准 [utility.arg.requirements], 表 23 [ISO/IEC 14882-2014] 规定了自赋值的后置条件:对于 x = y , y 值不变。当 &x == &y,该后置条件可以转换为 xy 的值依然不变。最初自赋值的实现,在资源拷贝到给定变量的过程中,会销毁局部对象资源。如果给定的变量和局部对象是同一个,销毁局部对象资源的行为将使其失效。后续那些资源将处于不确定的状态,违背了后置条件。

一个用户提供的拷贝赋值运算必须避免自拷贝赋值将对象遗留在一个中间状态。这可以通过自赋值测试,拷贝-交换,或者其他惯用的设计模式来实现。

C++ 标准,[copyassignable],规定类型必须确保当对象被传入标准模板库(STL)函数时,自拷贝赋值使对象处于一致状态。既然STL类型的对象被用于得是 CopyAssignable 的上下文中,STL类型也得需要优雅地处理自拷贝赋值。

不合规代码示例

在这个不合规代码例子中,拷贝赋值运算没有保护自拷贝赋值。如果自拷贝赋值发生,this->s1 将被删除,导致 rhs.s1 也被删除。接着,失效的 rhs.s1 内存被传入到 S 的拷贝构造函数中,导致解引用一个失效指针,invalid pointer.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <new>

struct S { S(const S &) noexcept; /* ... */ };

class T {
int n;
S *s1;

public:
T(const T &rhs) : n(rhs.n), s1(rhs.s1 ? new S(*rhs.s1) : nullptr) {}
~T() { delete s1; }

// ...

T& operator=(const T &rhs) {
n = rhs.n;
delete s1;
s1 = new S(*rhs.s1);
return *this;
}
};

合规方案 (Self-Test)

这个合规方案通过测试给定的参数是否和 this 相同来保护自拷贝赋值。如果自拷贝赋值出现,然后 operator= 什么都不做;否则,拷贝像原示例执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <new>

struct S { S(const S &) noexcept; /* ... */ };

class T {
int n;
S *s1;

public:
T(const T &rhs) : n(rhs.n), s1(rhs.s1 ? new S(*rhs.s1) : nullptr) {}
~T() { delete s1; }

// ...

T& operator=(const T &rhs) {
if (this != &rhs) {
n = rhs.n;
delete s1;
try {
s1 = new S(*rhs.s1);
} catch (std::bad_alloc &) {
s1 = nullptr; // For basic exception guarantees
throw;
}
}
return *this;
}
};

这个方案没有为拷贝赋值提供 强异常,strong exception 保证。特别地,如果在计算 new 表达式时造成异常,this 早已被修改。然而,该方案提供了基本的异常保证,因为没有资源泄露,且所有数据成员拥有合法值。因此,这段代码遵循ERR56-CPP. Guarantee exception safety

合规方案 (Copy and Swap)

该合规方案是通过构造一个rhs 的临时对象与 *this 交换来避免自赋值拷贝。这个兼容方案提供了强异常保证。因为,若当创建临时对象时,资源分配导致异常被抛出, swap() 将不会被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <new>
#include <utility>

struct S { S(const S &) noexcept; /* ... */ };

class T {
int n;
S *s1;

public:
T(const T &rhs) : n(rhs.n), s1(rhs.s1 ? new S(*rhs.s1) : nullptr) {}
~T() { delete s1; }

// ...

void swap(T &rhs) noexcept {
using std::swap;
swap(n, rhs.n);
swap(s1, rhs.s1);
}

T& operator=(T rhs) noexcept {
rhs.swap(*this);
return *this;
}
};

合规方案 (Move and Swap)

该兼容方案使用和前例合规方案中一样的类 ST,但是增加了如下公有构造函数和方法:

1
2
3
4
5
6
7
8
9
10
T(T &&rhs) { *this = std::move(rhs); }

// ... everything except operator= ..

T& operator=(T &&rhs) noexcept {
using std::swap;
swap(n, rhs.n);
swap(s1, rhs.s1);
return *this;
}

拷贝赋值运算使用 std::move() 而不是 swap() 来实现安全的自赋值和强异常保证。移动赋值运算利用移动(通过方法形参)和交换。

移动构造函数不是严格必需的,但是对于支持移动运算的类同时定义一个移动构造函数和一个移动赋值运算是惯例。

记住不像拷贝赋值运算,移动赋值运算函数签名接收指向该对象的非常量(non-const)引用——被移动的对象将被处于一种非特定但有效的状态。移动构造函数和拷贝构造函数也存在相同的差异。

Risk Assessment

Allowing a copy assignment operator to corrupt an object could lead to undefined behavior.

Rule Severity Likelihood Remediation Cost Priority Level
OOP54-CPP Low Probable High P2 L3

Automated Detection

Tool Version Checker Description
Astrée 20.10 **dangling_pointer_use **
Clang 9.0 (r361550) cert-oop54-cpp Checked by clang-tidy.
Helix QAC 2022.1 C++4072, C++4073, C++4075, C++4076
Klocwork 2022.1 CL.SELF-ASSIGN
Parasoft C/C++test 2021.2 CERT_CPP-OOP54-a Check for assignment to self in operator=
Polyspace Bug Finder R2021b CERT C++: OOP54-CPP Checks for copy assignment operators where self-assignment is not tested (rule partially covered)
PRQA QA-C++ 4.4 4072, 4073, 4075, 4076

Search for other vulnerabilities resulting from the violation of this rule on the CERT website.

This rule is a partial subset of OOP58-CPP. Copy operations must not mutate the source object when copy operations do not gracefully handle self-copy assignment, because the copy operation may mutate both the source and destination objects (due to them being the same object).

Bibliography

[Henricson 1997] Rule 5.12, Copy assignment operators should be protected from doing destructive actions if an object is assigned to itself
[ISO/IEC 14882-2014] Subclause 17.6.3.1, “Template Argument Requirements” Subclause 17.6.4.9, “Function Arguments”
[Meyers 2005] Item 11, “Handle Assignment to Self in operator=
[Meyers 2014]

img img img