C++ Best Practices: Is Deleting Both Const and Non-Const Copy Constructors Redundant?
When designing non-copyable classes in modern C++, it is common practice to explicitly disable copying using the = delete specifier introduced in C++11. However, a common point of confusion arises around parameter types: Is it redundant to delete both the non-const (Foo&) and const (const Foo&) overloads of the copy constructor and copy assignment operator?
The Short Answer
Yes, deleting both is redundant. To make a class completely non-copyable, you only need to delete the standard const Foo& overload:
class Foo {
public:
Foo(const Foo&) = delete;
Foo& operator=(const Foo&) = delete;
};Deleting Foo(Foo&) alongside Foo(const Foo&) provides no practical benefit and clutters your codebase with boilerplate code. However, deleting only the non-const version (Foo(Foo&) = delete;) is completely wrong and leaves your class copyable.
Why Deleting Foo(const Foo&) Is Sufficient
To understand why deleting only the const reference version works, you need to understand how the C++ compiler handles overload resolution and implicitly declared special member functions.
1. It Prevents the Implicit Generation of Copy Operations
According to the C++ standard, if you explicitly declare any copy constructor (even as = delete), the compiler will not implicitly generate another copy constructor. By writing Foo(const Foo&) = delete;, no other copy constructor will be generated behind the scenes.
2. Overload Resolution Binds to const &
A reference to const (const Foo&) can bind to both const and non-const lvalues. When you try to copy a non-const instance of Foo:
Foo a;
Foo b = a; // What constructor is called?Overload resolution checks the available candidates. The only declared copy constructor is Foo(const Foo&). Since a non-const lvalue can bind to a const &, this overload is selected. Because it is marked as = delete, the compiler produces a compilation error. Both const and non-const objects are completely prevented from being copied.
What Happens If You Only Delete Foo(Foo&)?
Deleting only the non-const reference version (Foo(Foo&) = delete;) is an anti-pattern. While it prevents copying non-const objects, it fails to make your class non-copyable in all scenarios:
class BrokenNonCopyable {
public:
BrokenNonCopyable() = default;
BrokenNonCopyable(BrokenNonCopyable&) = delete; // Only non-const deleted
};
void test() {
const BrokenNonCopyable c1;
// BrokenNonCopyable c2 = c1;
// Compiler error: no matching function for call, because no const copy ctor exists.
}While this prevents copying due to a missing overload, declaring Foo(Foo&) = delete; is technically a declared copy constructor, meaning it alters the standard generation rules and can result in confusing compiler error messages (e.g., "no viable candidate" instead of the clearer "use of deleted function").
What About the Copy Assignment Operator?
The exact same logic applies to operator=. A single deleted overload accepting a const reference prevents assignment from both const and non-const lvalues:
class Foo {
public:
Foo& operator=(const Foo&) = delete; // Completely sufficient
};When executing a = b; where b is non-const, the compiler selects operator=(const Foo&) and throws a compilation failure because it is marked deleted.
The "Machine Code" Perspective
If an AI or tool mentions that deleting both produces the "same machine code," that is because deleted functions do not generate machine code at all. They exist purely as compile-time constructs to enforce compile-time constraints. Adding a second deleted overload does not alter the output binary; it only makes the compiler's overload set larger during compilation.
Modern C++ Best Practice
To adhere to standard C++ idiom, keep your class definitions clean by only deleting the const& overloads, and don't forget to address move semantics if you want a strictly move-only or immovable type:
class StrictlyNonCopyable {
public:
StrictlyNonCopyable() = default;
// Disable copying
StrictlyNonCopyable(const StrictlyNonCopyable&) = delete;
StrictlyNonCopyable& operator=(const StrictlyNonCopyable&) = delete;
// Explicitly handle move semantics (or let them be suppressed)
StrictlyNonCopyable(StrictlyNonCopyable&&) = default;
StrictlyNonCopyable& operator=(StrictlyNonCopyable&&) = default;
};In summary: Stick to deleting Foo(const Foo&) and operator=(const Foo&). Deleting the non-const variants adds redundant code that provides zero additional type safety or performance benefits.