You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 64 Next »

Self-assignment can occur in situations of varying complexity, but essentially, all self-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-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 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-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 { /* ... */ }; // Has nonthrowing copy constructor
 
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-assignment by testing whether the given parameter is the same as this. If self-assignment occurs, then operator= does nothing; otherwise, the copy proceeds as in the original example.

#include <new>
 
struct S { /* ... */ }; // Has nonthrowing copy constructor
 
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;
  }
};

Note that 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 { /* ... */ }; // Has nonthrowing copy constructor
 
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=(const T &rhs) noexcept {
    T(rhs).swap(*this);
    return *this;
  }
};

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

Parasoft C/C++test9.5OOP-34 
PRQA QA-C++4.44072, 4073, 4075, 4076 

Related Vulnerabilities

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

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-assignment, because the copy operation may mutate both the source and destination objects (due to them being the same object).

Bibliography

[Henricson 97]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 05]Item 11, "Handle Assignment to Self in operator="
[Meyers 14] 

 


  • No labels