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.

TypeFunctionalityMoved-from State
std::unique_ptrMove 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_ptrMove construction, Move assignment from a std::unique_ptrThe 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_ptrMove 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_iosmove()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_filebufMove constructor, Move assignmentThe 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::threadMove constructor, Move assignmentThe 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_lockMove constructor, Move assignmentThe moved-from object is guaranteed to be in its default state, per [thread.lock.unique.cons], paragraphs 21 and 23.
std::shared_lockMove constructor, Move assignmentThe moved-from object is guaranteed to be in its default state, per [thread.lock.shared.cons], paragraphs 21 and 23.
std::promiseMove constructor, Move assignmentThe moved-from object is guaranteed not to have any shared state, per [futures.promise], paragraphs 6 and 8.
std::futureMove constructor, Move assignmentCalling valid() on the moved-from object is guaranteed to return false, per [futures.unique_future], paragraphs 8 and 11.
std::shared_futureMove 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_taskMove constructor, Move assignmentThe 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 Clang 3.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

Automated Detection

Tool

Version

Checker

Description

CodeSonar
8.1p0

LANG.MEM.NPD
LANG.MEM.UVAR

Null Pointer Dereference
Uninitialized Variable

Helix QAC

2024.1

DF4701, DF4702, DF4703


Parasoft C/C++test

2023.1

CERT_CPP-EXP63-a

Do not rely on the value of a moved-from object

Polyspace Bug Finder

R2023b

CERT C++: EXP63-CPPChecks for read operations that reads the value of a moved-from object (rule fully covered)
PVS-Studio

7.30

V1030

Related Vulnerabilities

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

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"



  

3 Comments

  1. Hello, I have a question about the first non-compliant test case.

    In the text above, it says that "However, because the object is moved and then reused ...". However, I think the function call "g(std::move(s));" actually doesn't trigger move semantics.

    Because the function "g" accepts a "std::string &&" as the argument, the "std::move" only did a type conversion, and no move-constructor or move-assignment is called.

    This piece of code is almost the same with:

    void g(std::string &v) { // lvalue reference
      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(s); // lvalue reference
      }
    }

    We can look at another simplified case like this "std::string && other = std::move(s);". In this case the rvalue reference does actually act as a lvalue reference.

    If we are going to trigger move semantics, we'd better change the function "g" to accepts a "std::string" type as below:

    void g(std::string v) { // accept a value
      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)); // this time s is actually "moved" into the function
      }
    }




    1. Agreed; the signature of g() is incorrect in both the NCCE and CS and should be std::string rather than std::string&&.

      1. Agreed, I've fixed both code examples.