In user code, you almost never need to write destructors and therefore copy/move constructors and assignment operators. This is because you will compose your classes of RAII-enabled objects.
The only time we need to write a destructor is if we are controlling a non-RAII resource, such as a file handle or database connection exported from a c-style or primitive c++ library.
In this case we simply wrap the resource in a unique_ptr (with custom deleter) or shared_ptr (again, with custom deleter) and we're done.
That leaves us with 2 more scenarios:
A base class of a polymorphic interface (which will not be controlled by a shared_ptr) - in which case we must write a virtual destructor and then possibly implement move/copy in terms of defaulted implementations.
A class which is moveable (because it owns a unique_ptr, say?) and we want it to be copyable. Now we are forced to implement the copy operations and default the move operations.
There are a few other corner cases, such as if your class owns a mutex, which is non-copyable. But if your class owns a mutex it's probably already a design error to require it to be copyable. In any case, by this time you ought to have learned the copy/assignment/move rules off by heart.
some examples:
struct owns_a_file_movable
{
struct file_deleter {
void operator()(FILE* f) const noexcept {
fclose(f);
}
};
// RAII class now, moveable. Copy would not make sense.
std::unique_ptr<FILE, file_deleter> file_;
};
struct owns_a_file_copyable
{
struct file_deleter {
void operator()(FILE* f) const noexcept {
fclose(f);
}
};
// construct from string
owns_a_file_copyable(std::string fname)
: path_(std::move(fname))
, file_(fopen(path_, "r"), file_deleter())
{
}
// we want it to be copyable. In our case, a copy will open another
// instance of the file. so we must store the filename.
owns_a_file_copyable(owns_a_file_copyable const& r)
: path_(t.path_)
, file_(fopen(path_, "r"), file_deleter())
{}
owns_a_file_copyable& operator=(owns_a_file_copyable const& r)
{
auto tmp = r;
std::swap(path_, tmp.path_); // changed: was r.path_ which was wrong
std::swap(file_, tmp.file_);
return *this;
}
owns_a_file_copyable(owns_a_file_copyable&&) = default;
owns_a_file_copyable& operator=(owns_a_file_copyable&&) = default;
// std::string is fully RAII
std::string path_;
// RAII class now, moveable
std::unique_ptr<FILE, file_deleter> file_;
};
struct how_most_classes_should_be
{
// no destructors, copy operators, assignment or move - it's all
// generated for you.
std::string this_;
std::string that_;
std::shared_ptr<const OtherThing> shared_other_; // note - shared semantics
std::function<void()> closure_; // these are copyable too
};
For athos
What triggers what?
// ordinary constructor
auto y = owns_a_file_copyable("/users/rhodges/foo.txt");
// copy constructor: owns_a_file_copyable(owns_a_file_copyable const&)
auto x = y;
// copy assignment: owns_a_file_copyable& operator-(owns_a_file_copyable const&)
x = y
// move-constructor: owns_a_file_copyable(owns_a_file_copyable &&)
auto z = std::move(x);
// move-assignment: owns_a_file_copyable& operator-(owns_a_file_copyable &&)
z = std::move(y);
// also move-constructor
extern owns_a_file_copyable make_file();
auto w = make_file();
// this time move-assignment
w = make_file();