Lesson 04 / 11
Non-Copyable and Non-Movable Types
Using = delete to forbid copying, moving, or both on a class; a resource emptying by a promise that comes from the class itself, not the standard.
Published Updated
In this lesson
- writes a class that forbids copying and moving separately with = delete
- explains how a class's own move constructor empties its resource
- distinguishes a move silently falling back to a copy from a deleted function failing at compile time
- builds an example confirming how a user-declared deleted copy constructor suppresses the implicit move constructor
Contents
The previous lesson showed that a resource’s post-move state is sometimes decided by the standard (std::string), and sometimes by the class itself (Record, PlainRecord). This lesson turns to the last kind, empty: just as the standard gives the nullptr promise for unique_ptr, a class’s own move constructor can also empty a resource explicitly and predictably. It also covers how = delete can close off copying and moving independently of each other.
The three lessons so far assumed a class was copyable and movable; the only question was which kind the post-move resource fell into. But for some classes, copying or moving itself makes no sense at all — copying a file handle can lead to two independent close calls, sharing ownership of a lock can lead multiple threads to believe they hold the same lock. This lesson covers = delete, the tool a class author uses to close off copying, moving, or both entirely, and the three combinations of that closure (non-copyable-but-movable, copyable-but-non-movable, neither).
Restricted Ownership with = delete
// handle.cpp — the cases arising from partially deleted special member functions
#include <cstdio>
#include <type_traits>
#include <utility>
class Handle {
public:
explicit Handle(int id) : id_(id) {}
Handle(const Handle&) = delete;
Handle& operator=(const Handle&) = delete;
Handle(Handle&& other) noexcept : id_(other.id_) {
other.id_ = -1;
}
Handle& operator=(Handle&&) = delete;
int id() const { return id_; }
private:
int id_;
};
static void handle_properties() {
std::printf("Handle copy-constructible: %s\n", std::is_copy_constructible_v<Handle> ? "yes" : "no");
std::printf("Handle move-assignable: %s\n", std::is_move_assignable_v<Handle> ? "yes" : "no");
std::printf("Handle move-constructible: %s\n", std::is_move_constructible_v<Handle> ? "yes" : "no");
}
static void handle_move() {
Handle a(7);
Handle b(std::move(a));
std::printf("b.id() = %d\n", b.id());
std::printf("a.id() (after the move) = %d\n", a.id());
}
class CopyOnly {
public:
explicit CopyOnly(int value) : value_(value) {}
CopyOnly(const CopyOnly&) = default;
CopyOnly& operator=(const CopyOnly&) = default;
CopyOnly(CopyOnly&&) = delete;
CopyOnly& operator=(CopyOnly&&) = delete;
private:
int value_;
};
static void copy_only_properties() {
std::printf("CopyOnly copy-constructible: %s\n", std::is_copy_constructible_v<CopyOnly> ? "yes" : "no");
std::printf("CopyOnly move-constructible: %s\n", std::is_move_constructible_v<CopyOnly> ? "yes" : "no");
}
class Lock {
public:
Lock() = default;
Lock(const Lock&) = delete;
Lock& operator=(const Lock&) = delete;
private:
int state_ = 0;
};
static void lock_properties() {
std::printf("Lock copy-constructible: %s\n", std::is_copy_constructible_v<Lock> ? "yes" : "no");
std::printf("Lock move-constructible: %s\n", std::is_move_constructible_v<Lock> ? "yes" : "no");
}
int main() {
handle_properties();
handle_move();
copy_only_properties();
lock_properties();
}
Handle copy-constructible: no Handle move-assignable: no Handle move-constructible: yes b.id() = 7 a.id() (after the move) = -1 CopyOnly copy-constructible: yes CopyOnly move-constructible: no Lock copy-constructible: no Lock move-constructible: no
OM7. Handle explicitly closes off its copy constructor and copy assignment with = delete — code trying to use either fails to compile. The move constructor, on the other hand, is written by hand: it moves the source’s id_ member into the target and writes -1 into the source. This comes from a different place than unique_ptr‘s nullptr promise: the one that empties unique_ptr is the standard itself, while the one that empties Handle is a value the class’s author chose (-1). Both fall into the empty kind, but the party making the promise differs — one is the standard, the other is the class. The move assignment operator, meanwhile, is also closed off with = delete: Handle can be constructed by moving but cannot be move-assigned onto an existing object. This asymmetry can be intentional — for instance, Handle might wrap a resource for which reassignment after initial construction makes no sense.
Handle‘s being non-copyable but movable is not a random combination; it is exactly the same pattern the standard library’s unique_ptr follows. unique_ptr also closes off its copy constructor and copy assignment with = delete, writes its move constructor and move assignment by hand, and hands over the resource (the raw pointer it holds) from source to target, leaving nullptr in the source. Handle pulling its id_ member to -1 is a small-scale repetition of unique_ptr pulling its pointer to nullptr: both enforce the rule “this resource can only have one owner at a time, but ownership can be transferred from one object to another” by forbidding copying entirely and explicitly allowing moving. This is the concrete method a class author uses when they want a unique_ptr-like ownership model: delete the copying functions, write the moving functions by hand, and pull the source to a predictable value in the source.
Copyable But Non-Movable: A Restriction in Reverse
CopyOnly becomes non-movable by a different route than the previous lesson’s Record. Record never declared its move functions at all; when a move was attempted, overload resolution silently fell back to the copy constructor, and the program compiled and ran. CopyOnly, on the other hand, declares and deletes its move functions. The difference shows up when a move is attempted: a deleted function fully participates in overload resolution, it is a candidate; if it is the best-matching candidate, the program does not compile. A line trying to initialize a CopyOnly object with std::move therefore does not silently fall to a copy as in Record‘s case, it produces a straight compile error. This lesson does not place that line in a separate code block — because a block that fails to compile would violate this course’s rule that every block is really compiled and run — but is_move_constructible_v<CopyOnly> printing no is indirect proof that this ban is real.
The distinction here is: if a move function does not exist at all, overload resolution falls back to another candidate that does exist (the copy constructor) — a silent, compiling result. If a move function exists but is deleted, overload resolution picks it as the best match and stops there — a noisy, non-compiling result. Both can be described as “non-movable,” but one silently routes the program to a different operation, the other keeps the program from compiling at all.
CopyOnly‘s copyable-but-non-movable combination is the reverse of Handle‘s, and is usually chosen for a different reason: if a class holds a resource whose value can be shared but whose ownership cannot be transferred (say, a fixed configuration value, or a small piece of data that is cheap to copy but whose moving carries no extra meaning), closing off the move functions and allowing only copying can be a deliberate design decision. This is clearly visible in the CopyOnly example, where the value_ member is an int: int is a movable type, the class closes off moving by its own choice, not because of its member’s type. A class’s movability, just like its copyability, follows from its own declared special member functions, not from its members’ movability.
Neither Copyable Nor Movable
For some types, neither copying nor moving makes sense — copying a lock’s ownership can lead to two independent locks, moving it can confuse which thread holds the lock. The standard library’s std::mutex is an example of this kind (this lesson does not teach concurrency, it only names it): both its copy and move functions are deleted, it is neither copyable nor movable.
These three examples — Handle (non-copyable, movable), CopyOnly (copyable, non-movable), std::mutex and Lock (neither copyable nor movable) — are three different answers to the same question: which ownership transfer makes sense for this type? That answer never comes from the calling code, a compiler flag, or a runtime setting; it is written into the class’s own definition, into its = default and = delete lines. To learn whether a class is copyable or movable, one does not need to read the code that uses that class — looking at the class’s own declaration is enough. This means ownership restrictions are declared in the type: the restriction lives not in a document, a comment, or a usage convention, but in a declaration the compiler itself can check.
The Lock class shows a subtler point here: the copy constructor is declared by hand with = delete, but neither of the move functions is ever written — neither = default nor = delete. Yet is_move_constructible_v<Lock> prints no.
OM8. The reason for this is that the rule for implicitly generating the move functions does not look at whether the copy constructor is deleted, only at whether it is declared: if a class declares its copy constructor, its copy assignment, or its destructor as a user — even if that declaration is = delete — the compiler no longer implicitly generates the move functions. Lock never mentioned moving at all, but became non-movable indirectly by deleting its copy constructor. This also sharpens the distinction between empty and unspecified: in both classes a value is taken from the source and carried to the target — the source loses its pre-move value, both are the same in that respect. The distinction is whether it is stated where the value the source is left with ends up. Empty is the state where the value the source will take after a move is explicitly promised by the standard or by the class itself, that is, where it is stated where it ends up (Handle’s -1, a promise from its own move constructor; unique_ptr’s nullptr, a promise from the standard). Unspecified is the state where no such promise is ever given, where it is not stated where the source ends up (std::string’s moved content — it is valid, but which value it holds is not specified). Lock‘s situation is neither of these — since moving is never possible at all, the question of the source’s “post-move state” is never even asked.
The Bounding Measurement
Lock‘s int member named state_ is a movable type, and the class itself contains no other explicit move ban — by member-based reasoning one might say “of course it is movable.” But the result is no. The reason is not the members’ type but a single special member function Lock itself declares: the deleted copy constructor. The rule seen throughout this lesson appears here in its most compressed form: the question of movability does not look at the members, it looks at whether any of the class’s “big three” (destructor, copy constructor, copy assignment) has been touched by the user. If it has been touched — even if deleted — moving is removed from implicit generation.
This rule shares the same root as the previous lesson’s Record example, but reaches the opposite outcome. Record removed moving from implicit generation by writing a destructor; overload resolution fell back to the copy constructor that still implicitly existed — the result was a silently running copy. Lock, on the other hand, removes moving from implicit generation by deleting the copy constructor; but this time there is no copy constructor left to fall back to either, because it too is deleted. The result is a class that is neither copyable nor movable. The same underlying rule — a user-declared special member function suppresses the implicit generation of the others — lands in two different directions on two different classes: a silent copy in one, a total shutdown in the other. This is the direct link the fourth lesson forms with the third: the same mechanism can produce a wildly different outcome depending on which function is touched.
These three combinations — Handle, CopyOnly, Lock — correspond to a layer beneath the question followed through this course’s four lessons (what an operation leaves in the source), namely the question “is this operation even possible for this resource at all.” The previous three lessons assumed moving was always possible and measured its outcome; this lesson showed that moving (or copying) might not be possible at all, and that this impossibility, just like a move’s outcome, is written into the class’s own declaration. Someone reading a class’s API can look at its = default and = delete lines and know in advance which transfers are possible with that class, which code pattern will fail to compile, which resource can be shared among how many objects — without needing to consult the class’s documentation or usage guide.
Summary
= deletecan close off copying and moving independently of each other;Handleshows a move-constructible-but-not-move-assignable, non-copyable combination.- A class’s own move constructor can explicitly empty a resource (
Handle’s-1); this is the same empty kind, but coming from a different source than a promise the standard gives (unique_ptr’s nullptr). - If a move function is never declared, overload resolution silently falls back to the copy constructor (
Record); if a move function is declared and deleted, the same call fails to compile (CopyOnly) — both are “non-movable” but their outcomes differ. - A user-declared copy constructor — even a deleted one — keeps the compiler from implicitly generating the move functions;
Lockbecomes non-movable without ever mentioning moving. - Some types (such as
std::mutex) are designed to be neither copyable nor movable; this means ownership must never be transferred for that type by any means.
Recognizing these three patterns also helps when encountering a type from a third-party library or the standard library: std::thread is non-copyable but movable, so is std::ifstream; std::atomic is neither copyable nor movable. None of this behavior can be guessed without reading documentation, but where the class’s = delete and = default lines are visible, it can be read directly from there.
Next Step
These four lessons measured the four possible outcomes of moving a single object (unchanged, changed, empty, unspecified) and where that outcome comes from — the standard, the class, or the operation’s own impossibility. The next topic carries this question from a single object to a plural structure: how a container holds its elements, and which kind the container itself and its elements fall into when an element is taken out of one container and moved into another.
To keep your progress and take notes, Log in
My notes
Log in to take notes.