Required and Guaranteed States of Moved-From Objects

Required States of Moved-From Objects

Destructible and Assignable

Assignable and Destructible Moved-From Objects

In most of the classes, the generated special move member functions bring moved-from objects into a state where the assignment operator and destructor work just fine. However, provided each moved-from member is assignable and destructible, both an assignment and the destruction of the moved-from object as a whole should work well:

Non-Destructible Moved-From Objects

Example:


#include <array>
#include <thread>

class Tasks {
private:
  std::array<std::thread,10> threads;  // array of threads for up to 10 tasks
  int numThreads{0};                   // current number of threads/tasks
public:
  Tasks() = default;

  // pass a new thread:
  template <typename T>
  void start(T op) {
    threads[numThreads] = std::thread{std::move(op)};
    ++numThreads;
  }
  //...

  // OOPS: enable default move semantics:
  Tasks(Tasks&&) = default;
  Tasks& operator=(Tasks&&) = default; 

  // at the end wait for all started threads:
  ~Tasks() {
    // numThreads and the number of active threads
    // in threads[] is inconsistent
    for (int i = 0; i < numThreads; ++i) {
      threads[i].join();
    }
  }
}; 
int main()
{
  try {
    Tasks ts;
    ts.start([]{
               std::this_thread::sleep_for(std::chrono::seconds{2});
               std::cout << "\nt1 done" << std::endl;
             });
    ts.start([]{
               std::cout << "\nt2 done" << std::endl;
             });

    // OOPS: move tasks:
    Tasks other{std::move(ts)};
  }
  catch (const std::exception& e) {
    std::cerr << "EXCEPTION: " << e.what() << std::endl;
  }
}

The move operations of containers move the elements, so that after the std::move(), ts no longer contains any thread objects that represent a running thread. Therefore, numThreads is just copied, which means that we create an inconsistent/invalid state. The destructor will finally loop over the first two elements calling join(), which throws an exception (which is a fatal error in a destructor).

Dealing with Borken Invariant

Breaking Invariant Due to Moved Member

class Card {
public:
   Card(const std::string& v) : _value(v)
   {
      assertValidCard(_value);
   }
   std::string getValue() const {
      return _value;
   }
private:
   std::string  _value;
};

In this class, one of the expected invariat is that _value should always follow a specific pattern. For example, _value = "queen-of-hearts". However, the _value in the moved_from object does not adhere to this pattern.

The options for fixing this class are as follow:

Beaking Invariants Due to Moved Consistent Value Members

Two member values are expected to be consistent. For example:

class IntString {}
private:
   int val;
   std::string sval;
public:
   IntString(int i=0) : val{i}, sval{std::to_string(i)}
   { }
   
   oid dump() {
      std::cout << val << "/" << sval << "\n";
   }
;

Beaking Invariants Due to Moved Point-Like Members

class SharedInt {
private:
   std::shared_ptr<int> sp;
public:
   explicit SharedInt(int val)
   : sp{std::make_shared<int>(val)} {}

   // Error: assumes there is always an int value
   std::string asString() const {
      return std::to_string(*sp);
   }
};

Resolutions: