Why Move-Only Types Matter

In high‑performance C++ code you often need objects that own a resource but must never be copied. A classic example is a wrapper around a file descriptor or a GPU buffer. Copying such a handle would duplicate ownership and lead to double‑close bugs. Making the type move‑only forces the compiler to enforce single ownership at compile time.

The Rule of Five in a Nutshell

When a class manages a resource you normally provide five special members: destructor, copy constructor, copy assignment, move constructor, and move assignment. For a move‑only type you delete the copy operations and implement the move operations yourself. The destructor releases the resource.

Real‑World Scenario: A Scoped Thread Pool Handle

Imagine a thread‑pool abstraction that hands out a TaskHandle to the caller. The handle stores an index into the pool’s internal queue. Only the thread that created the task may cancel it, so copying the handle would break that guarantee. By making TaskHandle move‑only you get compile‑time safety and zero‑overhead moves.

Production‑Ready Implementation

class TaskHandle {
public:
    // Construction from the pool – only the pool can create a valid handle
    explicit TaskHandle(std::size_t idx, ThreadPool* pool) noexcept
        : index_(idx), pool_(pool) {}

    // Delete copy operations
    TaskHandle(const TaskHandle&) = delete;
    TaskHandle& operator=(const TaskHandle&) = delete;

    // Move constructor – transfers ownership
    TaskHandle(TaskHandle&& other) noexcept
        : index_(other.index_), pool_(other.pool_) {
        other.index_ = invalid_index;
        other.pool_ = nullptr;
    }

    // Move assignment – strong exception guarantee
    TaskHandle& operator=(TaskHandle&& other) noexcept {
        if (this != &other) {
            release();
            index_ = other.index_;
            pool_ = other.pool_;
            other.index_ = invalid_index;
            other.pool_ = nullptr;
        }
        return *this;
    }

    // Destructor – returns the slot to the pool
    ~TaskHandle() noexcept {
        release();
    }

    // Public API
    bool valid() const noexcept { return pool_ != nullptr; }
    void cancel() noexcept {
        if (valid()) pool_->cancelTask(index_);
    }

private:
    static constexpr std::size_t invalid_index = static_cast(-1);
    std::size_t index_ = invalid_index;
    ThreadPool* pool_ = nullptr;

    void release() noexcept {
        if (valid()) {
            pool_->returnSlot(index_);
            index_ = invalid_index;
            pool_ = nullptr;
        }
    }
};

Why This Works

  • Deleted copy ops make any attempt to copy a compile error, catching bugs early.
  • Move ops are noexcept so containers like std::vector can relocate elements without falling back to copies.
  • Self‑assignment guard in move assignment prevents double release when a handle is moved onto itself.
  • Release logic centralized in a private release() method avoids duplication and guarantees the resource is returned exactly once.

Common Pitfalls and How to Avoid Them

Forgetting to reset the source object’s members after a move leaves a dangling pointer that the destructor will later try to release. The pattern above sets both index_ and pool_ to sentinel values. Another mistake is making the move constructor explicit; that prevents implicit moves in return statements and forces unnecessary std::move calls. Keep move constructors implicit.

Extending the Pattern

If the managed resource requires a custom deleter (e.g., a Vulkan buffer), store a std::function or a small lambda in the handle and invoke it from release(). The same move‑only skeleton applies, only the cleanup code changes.

Final Thoughts

Move‑only types are a cornerstone of modern C++ resource management. By following the Rule of Five with deleted copies and well‑behaved moves you get deterministic ownership, no hidden copies, and code that scales cleanly across threads. The TaskHandle example is a template you can drop into any codebase that hands out opaque tokens – just replace the pool interaction with your own resource logic.