CertC++-EXP63¶
Do not rely on the value of a moved-from object
Required inputs: IR
Many types, including user-defined types and types provided by the Standard Template Library, support move semantics. Except in rare circumstances, an object of a type that supports move operations (move initialization or move assignment) will be left in a valid, but unspecified state after the object's value has been moved.
Passing an object as a function argument that binds to an rvalue reference parameter, including via implicit function call syntax such as move assignment or move construction, moves the object's state into another object. Upon return from such a function call, the object that was bound as an rvalue reference parameter is considered to be in the moved-from state. Once an object is in the moved-from state, the only operations that may be safely performed on that object instance are ones for which the operation has no preconditions, because it is unknown whether the unspecified state of the object will satisfy those preconditions. While some types have explicitly-defined preconditions, such as types defined by the Standard Template Library, it should be assumed that the only operations that may be safely performed on a moved-from object instance are reinitialization through assignment into the object or terminating the lifetime of the object by invoking its destructor.
Do not rely on the value of a moved-from object unless the type of the object is documented to be in a well-specified state. While the object is guaranteed to be in a valid state, relying on unspecified values leads to unspecified behavior. Since the behavior need not be documented, this can in turn result in abnormal program behavior and portability concerns.
The following Standard Template Library functions are guaranteed to leave the moved-from object in a well-specified state.
| Type | Functionality | Moved-from State |
|---|---|---|
std::unique_ptr |
Move construction, Move assignment, "Converting" move construction, "Converting" move assignment (likewise for std::unique_ptr for array objects with a runtime length)
|
The moved-from object is guaranteed to refer to a null pointer value, per [unique.ptr], paragraph 4 [ ISO/IEC 14882-2014]. |
std::shared_ptr |
Move construction, Move assignment, "Converting" move construction, "Converting" move assignment |
The moved-from object shall be "empty," per [util.smartptr.shared.const], paragraph 22 and [util.smartptr.shared.assign], paragraph 4. |
std::shared_ptr |
Move construction, Move assignment from a
std::unique_ptr
|
The moved-from object is guaranteed to refer to a null pointer value, per [util.smartptr.shared.const], paragraph 29 and [util.smartptr.shared.assign], paragraph 6. |
std::weak_ptr |
Move construction, Move assignment, "Converting" move construction, "Converting" move assignment |
The moved-from object shall be "empty," per [util.smartptr.weak.const], paragraph 8, and [util.smartptr.weak.assign], paragraph 4. |
std::basic_ios |
move() |
The moved-from object is still left in an unspecified state, except that
rdbuf() shall return the same value as it returned before the
move, and
tie() shall return
0, per [basic.ios.members], paragraph 20.
|
std::basic_filebuf |
Move constructor, Move assignment | The moved-from object is guaranteed to reference no file; other internal state is also affected, per [filebuf.cons], paragraphs 3 and 4, and [filebuf.assign], paragraph 1. |
std::thread |
Move constructor, Move assignment | The result from calling
get_id() on the moved-from object is guaranteed to remain
unchanged; otherwise the object is in an unspecified state, per
[thread.thread.constr], paragraph 11 and [thread.thread.assign], paragraph 2.
|
std::unique_lock |
Move constructor, Move assignment | The moved-from object is guaranteed to be in its default state, per [thread.lock.unique.cons], paragraphs 21 and 23. |
std::shared_lock |
Move constructor, Move assignment | The moved-from object is guaranteed to be in its default state, per [thread.lock.shared.cons], paragraphs 21 and 23. |
std::promise |
Move constructor, Move assignment | The moved-from object is guaranteed not to have any shared state, per [futures.promise], paragraphs 6 and 8. |
std::future |
Move constructor, Move assignment | Calling
valid() on the moved-from object is guaranteed to return
false, per [futures.unique_future], paragraphs 8 and 11.
|
std::shared_future |
Move constructor, Move assignment, "Converting" move constructor, "Converting" move assignment |
Calling
valid() on the moved-from object is guaranteed to return
false, per [futures.shared_future], paragraphs 8 and
11.
|
std::packaged_task |
Move constructor, Move assignment | The moved-from object is guaranteed not to have any shared state, per [future.task.members], paragraphs 7 and 8. |
Several generic standard template library (STL) algorithms, such
as
std::remove() and
std::unique(), remove instances of elements from a container
without shrinking the size of the container. Instead, these algorithms return
a
ForwardIterator to indicate the partition within the
container after which elements are no longer valid. The elements in the
container that precede the returned iterator are valid elements with specified
values; whereas the elements
that succeed the returned iterator are
valid but have unspecified values. Accessing
unspecified
values of elements iterated over results in
unspecified
behavior. Frequently, the
erase-remove idiom is used to shrink the size of
the container when using these algorithms.
Noncompliant Code Example
In this noncompliant code example, the integer values
0 through
9 are expected to be printed to the standard output stream
from a
std::string rvalue reference. However, because
the object is moved and then reused under the assumption its internal
state has been cleared, unexpected output may occur despite not
triggering
undefined
behavior.
#include <iostream>
#include <string>
void g(std::string v) {
std::cout << v << std::endl;
}
void f() {
std::string s;
for (unsigned i = 0; i < 10; ++i) {
s.append(1, static_cast<char>('0' + i));
g(std::move(s));
}
}
Implementation Details
Some standard library implementations may implement the short string
optimization (SSO) when implementing
std::string. In such implementations, strings under a certain
length are stored in a character buffer internal to the
std::string object (avoiding an expensive heap allocation
operation). However, such an implementation might not alter the original buffer
value when performing a move operation. When the noncompliant code example is
compiled with
Clang3.7 using
libc++,
the following output is produced.
0 01 012 0123 01234 012345 0123456 01234567 012345678 0123456789
Compliant Solution
In this compliant solution, the
std::string object is initialized to the expected value on
each iteration of the loop. This practice ensures that the object is in a
valid, specified state prior to attempting to access it in
g(), resulting in the expected output.
#include <iostream>
#include <string>
void g(std::string v) {
std::cout << v << std::endl;
}
void f() {
for (unsigned i = 0; i < 10; ++i) {
std::string s(1, static_cast<char>('0' + i));
g(std::move(s));
}
}
Noncompliant Code Example
In this noncompliant code example,
elements matching
42 are removed from the given container. The contents of the
container are then printed to the standard output stream. However, if any
elements were removed from the container, the range-based
for loop iterates over an invalid iterator range, resulting
in
unspecified
behavior.
#include <algorithm>
#include <iostream>
#include <vector>
void f(std::vector<int> &c) {
std::remove(c.begin(), c.end(), 42);
for (auto v : c) {
std::cout << "Container element: " << v << std::endl;
}
}
Compliant Solution
In this compliant solution, elements removed by the standard algorithm are skipped during iteration.
#include <algorithm>
#include <iostream>
#include <vector>
void f(std::vector<int> &c) {
auto e = std::remove(c.begin(), c.end(), 42);
for (auto i = c.begin(); i != c.end(); i++) {
if (i < e) {
std::cout << *i << std::endl;
}
}
}
Compliant Solution
In this compliant solution,
elements removed by the standard algorithm are subsequently erased from the
given container. This technique ensures that a valid iterator range is used by
the range-based
for loop.
#include <algorithm>
#include <iostream>
#include <vector>
void f(std::vector<int> &c) {
c.erase(std::remove(c.begin(), c.end(), 42), c.end());
for (auto v : c) {
std::cout << "Container element: " << v << std::endl;
}
}
Risk Assessment
The state of a moved-from object is generally valid, but unspecified. Relying on unspecified values can lead to abnormal program termination as well as data integrity violations.
| Rule | Severity | Likelihood | Remediation Cost | Priority | Level |
|---|---|---|---|---|---|
| EXP63-CPP | Medium | Probable | Medium | P8 | L2 |
Related Guidelines
Bibliography
| [ ISO/IEC 14882-2014] | Subclause 17.6.5.15, "Moved-from State of Library Types" Subclause 20.8.1, "Class Template unique_ptr"Subclause 20.8.2, "Shared-Ownership Pointers" Subclause 27.5.5, "Class Template basic_ios"Subclause 27.9.1, "File Streams" Subclause 30.3.1, "Class thread"Subclause 30.4.2, "Locks" Subclause 30.6, "Futures" |
Possible Messages
Key |
Text |
Severity |
Disabled |
|---|---|---|---|
lval_param_moved_at_end |
Parameter of lvalue-reference-type left in moved-from state when function returns |
None |
False |
moved_from_read |
Don’t read-access a moved-from object |
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
moving_functions¶
moving_functions : set[bauhaus.analysis.config.QualifiedName] = set()
read_move_function_exceptions¶
read_move_function_exceptions
Names of functions that are considered to leave the moved from objects in a well-specified state and are therefore except.Type: set[bauhaus.analysis.config.QualifiedName]
Default:
{'std::basic_filebuf::basic_filebuf', 'std::basic_filebuf::operator=', 'std::future::future', 'std::future::operator=', 'std::get', 'std::make_shared', 'std::make_unique', 'std::packaged_task::operator=', 'std::packaged_task::packaged_task', 'std::promise::operator=', 'std::promise::promise', 'std::shared_future::operator=', 'std::shared_future::shared_future', 'std::shared_lock::operator=', 'std::shared_lock::shared_lock', 'std::shared_ptr::operator=', 'std::shared_ptr::shared_ptr', 'std::thread::operator=', 'std::thread::thread', 'std::unique_lock::operator=', 'std::unique_lock::unique_lock', 'std::unique_ptr::operator=', 'std::unique_ptr::unique_ptr', 'std::weak_ptr::operator=', 'std::weak_ptr::weak_ptr'}
read_move_type_exceptions¶
read_move_type_exceptions : set[bauhaus.analysis.config.QualifiedName] = {'std::basic_ios'}
restore_subobjects_with_enclosing_objects¶
restore_subobjects_with_enclosing_objects : bool = True
restoring_functions¶
restoring_functions
Names of functions that are considered to restore an object to a well-specified state.Type: set[bauhaus.analysis.config.QualifiedName]
Default:
{'std::basic_string::clear', 'std::deque::clear', 'std::forward_list::clear', 'std::list::clear', 'std::map::clear', 'std::multimap::clear', 'std::multiset::clear', 'std::set::clear', 'std::unordered_map::clear', 'std::unordered_multimap::clear', 'std::unordered_multiset::clear', 'std::unordered_set::clear', 'std::vector::clear'}
use_static_semantic_analysis¶
use_static_semantic_analysis : bool = True
StaticSemanticAnalysis to be enabled, but also
will not produce any additional results if it is not.