skip to content
Mehdi Mehdikhani
Table of Contents

The = delete syntax in C++11 lets you explicitly delete functions, giving much better error messages than the old private declaration trick. It’s cleaner and more expressive.

What Deleted Functions Actually Do

= delete tells the compiler to reject any attempts to call a function:

class NonCopyable {
public:
NonCopyable() = default;
// Explicitly delete copy operations
NonCopyable(const NonCopyable&) = delete;
NonCopyable& operator=(const NonCopyable&) = delete;
};
NonCopyable obj;
NonCopyable copy = obj; // Clear error: function is deleted

Before C++11, you’d make these private and not implement them - much more confusing.

Why This Is Better

The old way was hacky and gave poor error messages:

// Old C++98 way
class NonCopyable {
private:
NonCopyable(const NonCopyable&); // Declared but not implemented
NonCopyable& operator=(const NonCopyable&); // Hope nobody calls these!
};

If someone accidentally tried to copy, they’d get a linker error instead of a clear compile error.

When I Actually Use Deleted Functions

Most of the time, I use = delete for:

  1. Preventing copying: For RAII classes that manage unique resources:
class FileHandle {
FILE* file_;
public:
explicit FileHandle(const char* filename)
: file_(fopen(filename, "r")) {}
~FileHandle() {
if (file_) fclose(file_);
}
// Can't copy file handles
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
// But moving is fine
FileHandle(FileHandle&& other) noexcept : file_(other.file_) {
other.file_ = nullptr;
}
};
  1. Preventing unwanted conversions: Avoiding implicit conversions that don’t make sense:
class UserId {
int id_;
public:
explicit UserId(int id) : id_(id) {}
// Don't allow accidental conversion from other types
UserId(double) = delete; // No floating point user IDs
UserId(const char*) = delete; // No string user IDs
int value() const { return id_; }
};
UserId user1(42); // OK
UserId user2(3.14); // Error: deleted function
UserId user3("admin"); // Error: deleted function
  1. Template specializations: Preventing certain template instantiations:
template<typename T>
void process_data(T data) {
// Generic implementation
}
// Delete specialization for dangerous types
template<>
void process_data<char*>(char*) = delete; // Use std::string instead
template<>
void process_data<void*>(void*) = delete; // Too unsafe
  1. Overload resolution control: Controlling which overloads are available:
class Logger {
public:
void log(const std::string& message);
void log(int level, const std::string& message);
// Don't allow logging raw pointers accidentally
void log(const char*) = delete; // Force conversion to string
void log(void*) = delete; // Probably a mistake
};
Logger logger;
logger.log("Hello"); // Error - use std::string
logger.log(std::string("Hi")); // OK

Deleted Special Member Functions

The rule of five becomes clearer with deleted functions:

class Resource {
void* data_;
public:
Resource();
~Resource();
// Either implement all copy/move operations or delete them
Resource(const Resource&) = delete;
Resource& operator=(const Resource&) = delete;
Resource(Resource&&) = delete;
Resource& operator=(Resource&&) = delete;
};

This makes intent crystal clear - this class doesn’t support any copying or moving.

Real World Example

Here’s how I use deleted functions in a thread-safe singleton:

class DatabaseConnection {
private:
static std::unique_ptr<DatabaseConnection> instance_;
static std::mutex mutex_;
// Private constructor
DatabaseConnection() = default;
public:
static DatabaseConnection& get_instance() {
std::lock_guard<std::mutex> lock(mutex_);
if (!instance_) {
instance_ = std::unique_ptr<DatabaseConnection>(new DatabaseConnection());
}
return *instance_;
}
// Delete all copy and move operations
DatabaseConnection(const DatabaseConnection&) = delete;
DatabaseConnection& operator=(const DatabaseConnection&) = delete;
DatabaseConnection(DatabaseConnection&&) = delete;
DatabaseConnection& operator=(DatabaseConnection&&) = delete;
void execute_query(const std::string& sql) {
// Implementation...
}
};

Deleted vs Private

Deleted functions participate in overload resolution, private functions don’t:

class Test {
public:
void func(int x);
void func(double x) = delete;
private:
void func(char x);
};
Test t;
t.func(42); // Calls func(int)
t.func(3.14); // Error: deleted function
t.func('a'); // Error: private function

The deleted function is considered during overload resolution and then rejected. The private function isn’t considered at all.

Template Function Deletion

You can delete specific template instantiations:

template<typename T>
void dangerous_operation(T* ptr) {
// Generic pointer operation
}
// Delete for specific dangerous types
template<>
void dangerous_operation<void>(void*) = delete;
template<>
void dangerous_operation<const char>(const char*) = delete;

The Pattern I Follow

I use = delete to be explicit about what operations are not supported:

// For RAII classes - usually delete copy, allow move
class UniqueResource {
public:
UniqueResource(UniqueResource&&) = default;
UniqueResource& operator=(UniqueResource&&) = default;
UniqueResource(const UniqueResource&) = delete;
UniqueResource& operator=(const UniqueResource&) = delete;
};
// For value types - usually allow everything or delete everything
class ImmutableValue {
public:
ImmutableValue(const ImmutableValue&) = default;
ImmutableValue& operator=(const ImmutableValue&) = delete; // Immutable!
};

Deleted functions make code more self-documenting and give better error messages. It’s a small feature, but it does a lot to express design intent clearly.