Hacker News new | ask | show | jobs
by asveikau 3358 days ago
If you implement a move as a swap (as in the examples I saw when I first heard of move constructors back when it was called c++0x) you don't need an empty state.

Not being able to keep track of what cases call a destructor and which ones do not sounds like a much more huge problem than the one you are talking about. Doesn't seem too hard to avoid the temptation throw in a move constructor.

3 comments

You can't always swap. For example, you may want to use a movable but non-copyable type to keep un-aliased access to a single resource (see linear/affine types) and the compiler treating the move source as dead is useful there. Or maybe you don't want to allocate anything for the move target because the resource you're managing is expensive.

Keeping track of when a destructor gets called is not a huge problem at all. From the user perspective it's airtight, proper uninitialized value analysis in the compiler make it impossible to screw up. From the implementation perspective all you need is the occasional flag on the stack (whose value is already calculated) for situations like `if ... { use_by_move(obj) }`, and a rule against moving things out of fields that you're otherwise not responsible for destructing (you can still swap there).

> If you implement a move as a swap (as in the examples I saw when I first heard of move constructors back when it was called c++0x) you don't need an empty state.

You do to e.g. default construct the temporary you'll be swapping with in the first place. Problematic if you're trying to construct e.g. a reference-like type with no null state.

How do you handle mode as a swap for something like a socket, or a file handle in that case?
These typically have null-like states in C++. If we were making a std::ifstream clone without templates inheritence, etc. we might write...

  class ifstream {
    FILE* f;
    ifstream(const ifstream&) = delete;
    ifstream& operator=(const ifstream&) = delete;
  public:
    ifstream(): f() {}
    explicit ifstream(const char* path): f(fopen(path, "rb")) {}
    ~ifstream() { if (f) fclose(f); }
    
    ifstream(ifstream&& original): f() {
      std::swap(f, original.f);
    }
    
    ifstream& operator=(ifstream&& original) {
      std::swap(f, original.f);
      return *this;
    }
    
    // ...
  };
This is very similar to the common pattern of implementing vanilla assignment in terms of copy construction and a swap.

EDIT: Added deleted copy ctor/assignment ops because I'm not a savage.

In the move constructor of the object wrapping an open file, swap the integer descriptors.