This rule is part of MISRA C++:2023.
Usage of this content is governed by Sonar’s terms and conditions. Redistribution is
prohibited.
Dir 15.8.1 - User-provided copy assignment operators and move assignment operators shall handle self-assignment
[swappable.requirements]
[moveassignable]
[copyassignable]
Category: Required
Amplification
Types supporting copy assignment shall satisfy the CopyAssignable requirement.
Types supporting move assignment shall satisfy the MoveAssignable requirement.
Additionally, in the case of self-assignment, user-provided copy assignment operators and move assignment operators
shall:
- Not have undefined behaviour; and
- Not leak any resources; and
- Preserve the value of the self-assigned object.
Note: what constitutes the value of an object depends on a class’s design, and is usually related to the semantics of the equality
operator.
Rationale
Class designs that require user-provided copy assignment operators or move assignment operators can be avoided when it
is possible to use a manager class [1] (see M23_372: MISRA C++ 2023 Rule 15.0.1), such as smart pointers and the containers
provided by the C++ Standard Library. However, when implementing a manager class [1], care needs to be taken when defining
user-provided copy assignment operators and move assignment operators.
Naïve implementations, particularly in the presence of self-assignment, can lead to undefined behaviour, resource leaks, performance
issues and unintended violations of the object’s semantics. Self-assignment is rarely intentional, but it is often hard to spot when it occurs — for
example, when manipulating overlapping ranges of objects.
This directive extends the CopyAssignable and MoveAssignable requirements to all types supporting these assignments, and
additionally requires that the state of the object is preserved after a self-assignment. This is done to ensure that the behaviour is predictable and
that no resources are leaked.
Well-known idioms, such as copy-and-swap, may help when complying with this directive. However, as there is no one-solution-fits-all, this
directive does not recommend a specific idiom.
Example
The following is a simplified implementation of a container, similar to std::vector. The class implements a general manager
[2], and so user-provided copy assignment operators and move assignment operators are required.
class Vector
{
std::size_t size_;
int32_t * buffer_;
public:
Vector() : size_( 0 ), buffer_( nullptr ) {}
Vector( std::size_t size ) : size_( size ), buffer_( new int[ size ] ) {}
~Vector()
{
delete[] this->buffer_;
}
Vector( Vector const & other ) :
size_( other.size_ ),
buffer_( other.size_ != 0 ? new int32_t[ other.size_ ] : nullptr )
{
( void )std::copy_n( other.buffer_, size_, this->buffer_ );
}
Vector( Vector && other ) noexcept :
size_( std::exchange( other.size_, 0 ) ),
buffer_( std::exchange( other.buffer_, nullptr ) )
{}
Vector & operator=( Vector const & other ) &;
Vector & operator=( Vector && other ) & noexcept;
};
Heading: Copy assignment
The following implementation of copy assignment is non-compliant due to the presence of undefined behaviour for self-assignment.
Vector & Vector::operator=( Vector const & other ) &
{
this->size_ = other.size_;
delete[] this->buffer_; // Deletes other.buffer_
this->buffer_ = new int[ other.size_ ]; // Reading from deleted storage,
// resulting in undefined behaviour
( void )std::copy_n( other.buffer_,
other.size_,
this->buffer_ );
return *this;
}
This undefined behaviour can be prevented by introducing a check for self-assignment.
Vector & Vector::operator=( Vector const & other ) &
{
if ( this != std::addressof( other ) )
{
this->size_ = other.size_;
delete[] this->buffer_;
this->buffer_ = new int[ other.size_ ];
( void )std::copy_n( other.buffer_,
other.size_,
this->buffer_ );
}
return *this;
}
The check for self-assignment is a valid solution in this case, but it does not guarantee a correct implementation in all cases (e.g.,
self-referential data structures). It also has several disadvantages which are outside of the scope of this directive, but which may need to be
considered in the final design:
- Pessimization of performance for the (presumably) rare case of self-assignment; and
- Code duplication within the destructor (deletion of elements and the buffer) and the copy constructor (deep copy of elements); and
- Failure to provide the strong exception safety guarantee.
These concerns are addressed when using the copy-and-swap idiom.
Vector & Vector::operator=( Vector const & other ) &
{
Vector tmp( other ); // Copy construction, with deep copying
std::swap( this->size_, tmp.size_ );
std::swap( this->buffer_, tmp.buffer_ );
return *this;
// tmp goes out of scope and thus takes care of deleting the previous buffer
}
Self-assignment is handled appropriately when using the copy-and-swap idiom. However, the creation of a new buffer invalidates any iterators or
references to elements of the original Vector, and it requires unnecessary duplication of resources. Whilst not shown in the above, these
issues can be avoided by introducing a check for self-assignment around the copy-and-swap algorithm.
Heading: Move assignment
The following implementation of move assignment has no undefined behaviour, but the Vector will be released when
self-assignment takes place. By any reasonable notion of equivalence, the value is not preserved.
Vector & Vector::operator=( Vector && other ) & noexcept
{
delete[] this->buffer_;
this->size_ = std::exchange( other.size_, 0 );
this->buffer_ = std::exchange( other.buffer_, nullptr );
return *this;
}
The following example addresses these issues by using the move-and-swap idiom.
Vector & Vector::operator=( Vector && other ) & noexcept
{
Vector tmp( std::move( other ) );
std::swap( this->size_, tmp.size_ );
std::swap( this->buffer_, tmp.buffer_ );
return *this;
}
When self-assignment takes place, the call to std::move transfers ownership of the resources to the temporary object tmp,
and then the calls to std:swap returns their ownership back to *this. There are no changes to the state of
*this and duplication of resources does not occur. Whilst not shown, a check for self-assignment may be included to avoid unnecessary
operations.
Glossary
[1] Manager class
A class that is either a scoped manager [3], a unique manager [4], or a general manager [2] as defined in
M23_372: MISRA C++ 2023 Rule 15.0.1.
[2] General manager
A manager class [1], as defined in M23_372: MISRA C++ 2023 Rule 15.0.1.
[3] Scoped manager
A manager class [1], as defined in M23_372: MISRA C++ 2023 Rule 15.0.1.
[4] Unique manager
A manager class [1], as defined in M23_372: MISRA C++ 2023 Rule 15.0.1.
Copyright The MISRA Consortium Limited © 2023