CertC++-OOP54¶
Gracefully handle self-copy assignment
Required inputs: IR
Self-copy assignment can occur in situations of varying complexity, but essentially, all self-copy assignments entail some variation of the following.
#include <utility>
struct S { /* ... */ }
void f() {
S s;
s = s; // Self-copy assignment
}
User-provided copy operators must properly handle self-copy assignment.
The postconditions required for copy assignment are specified by the C++
Standard, [utility.arg.requirements], Table 23 [
ISO/IEC
14882-2014], which states that for
x = y, the value of
y is unchanged. When
&x == &y, this postcondition translates into the values of
both
x and
y remaining unchanged. A naive implementation of copy
assignment could destroy object-local resources in the process of copying
resources from the given parameter. If the given parameter is the same object
as the local object, the act of destroying object-local resources will
invalidate them. The subsequent copy of those resources will be left in an
indeterminate state, which violates the postcondition.
A user-provided copy assignment operator must prevent self-copy assignment from leaving the object in an indeterminate state. This can be accomplished by self-assignment tests, copy-and-swap, or other idiomatic design patterns.
The C++ Standard, [copyassignable], specifies that types must ensure that
self-copy assignment leave the object in a consistent state when passed to
Standard Template Library (STL) functions. Since objects of STL types are used
in contexts where
CopyAssignable is required, STL types are required to gracefully
handle self-copy assignment.
Noncompliant Code Example
In this noncompliant code example, the copy assignment operator does not
protect against self-copy assignment. If self-copy assignment occurs,
this->s1 is deleted, which results in
rhs.s1 also being deleted. The invalidated memory for
rhs.s1 is then passed into the copy constructor for
S, which can result in dereferencing an
invalid
pointer.
#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;
}
};
Compliant Solution (Self-Test)
This compliant solution guards against self-copy assignment by testing whether
the given parameter is the same as
this. If self-copy assignment occurs, then
operator= does nothing; otherwise, the copy proceeds as in
the original example.
#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;
}
};
This solution does not provide a
strong
exception guarantee for the copy assignment. Specifically, if an exception
is called when evaluating the
new expression,
this has already been modified. However, this solution does
provide a basic exception guarantee because no resources are leaked and all
data members contain valid values. Consequently, this code complies with
ERR56-CPP.
Guarantee exception safety.
Compliant Solution (Copy and Swap)
This compliant solution avoids self-copy assignment by constructing a temporary
object from
rhs that is then swapped with
*this. This compliant solution provides a strong exception
guarantee because
swap() will never be called if resource allocation results in an
exception being thrown while creating the temporary object.
#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;
}
};
Compliant Solution (Move and Swap)
This compliant solution uses the same classes
S and
T from the previous compliant solution, but adds the following
public constructor and methods:
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;
}
The copy assignment operator uses
std::move() rather than
swap() to achieve safe self-assignment and a strong exception
guarantee. The move assignment operator uses a move (via the method parameter)
and swap.
The move constructor is not strictly necessary, but defining a move constructor along with a move assignment operator is conventional for classes that support move operations.
Note that unlike copy assignment operators, the signature of a move assignment operator accepts a non-const reference to its object with the expectation that the moved-from object will be left in an unspecified, but valid state. Move constructors have the same difference from copy constructors.
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 |
Related Guidelines
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] |
Possible Messages
Key |
Text |
Severity |
Disabled |
|---|---|---|---|
incomplete_swap |
Swap does not handle all non-static data members. |
None |
False |
self_assignment |
Missing or non-compliant guard against self-assignment. |
None |
False |
Options¶
This rule shares the following common options: exclude_in_macros, exclude_messages_in_system_headers, excludes, extend_exclude_to_macro_invocations, includes, justification_checker, languages, post_processing, provider, report_at, severity
The following places define options that affect this rule: Stylechecks, Analysis-GlobalOptions
allowed_comparison_operators¶
allowed_comparison_operators : set[bauhaus.ir.PIR_Class_Name] = {'Equal', 'Unequal'}
self_assignment_check_must_be_first_statement¶
self_assignment_check_must_be_first_statement : bool = False