A well-known idiom for making no-copy types is to create a base class
struct NoCopy {
NoCopy(){}
NoCopy(const NoCopy&) = delete;
NoCopy& operator=(const NoCopy&) = delete;
};
And derive from this, like so
struct Foo : NoCopy {
Foo(){}
};
Which will make the following fail to compile
Foo f;
Foo f2 = f;
But how do I enforce this? Any derived class can do the following
struct Foo2 : NoCopy {
Foo2(){}
Foo2(const Foo2&){}
};
Which is perfectly legal but makes no sense, I have now a type which is both copyable and also not copyable (through its base class).
How do I avoid this?
If there is no copy constructor, c++ creates a default copy constructor which makes a shallow copy. If the object has no pointers to dynamically allocated memory then shallow copy will do.
A trivially copyable class is a class that: has no non-trivial copy constructors, has no non-trivial move constructors, has no non-trivial copy assignment operators, has no non-trivial move assignment operators, and has a trivial destructor.
Trivial typesWhen a class or struct in C++ has compiler-provided or explicitly defaulted special member functions, then it is a trivial type. It occupies a contiguous memory area. It can have members with different access specifiers. In C++, the compiler is free to choose how to order members in this situation.
This is C++. In the world of template meta-programming almost anything is possible. If we make NoCopy
a CRTP base, we can add static assertions in its destructor.
template<class C>
struct NoCopy {
NoCopy(){}
NoCopy(const NoCopy&) = delete;
NoCopy& operator=(const NoCopy&) = delete;
~NoCopy() noexcept {
static_assert(std::is_base_of<NoCopy, C>::value, "CRTP not observed");
static_assert(!std::is_copy_constructible<C>::value, "A non-copyable copyable class? Really?");
}
};
Here's your code, adapted for a live example.
That's not without a price though, since now the class is not trivially destructible, and as such neither will be any class that derives from it. Whether or not it's acceptable is up to you.
Upon further reflection, if you provide only a single way to intiialize your class, then the default constructor has to be referred to and called. So the static assertion can be moved there, and the type is back to being trivially destructible:
template<class C>
struct NoCopy {
NoCopy() noexcept {
static_assert(std::is_base_of<NoCopy, C>::value, "CRTP not observed");
static_assert(!std::is_copy_constructible<C>::value, "A non-copyable copyable class? Really?");
}
NoCopy(const NoCopy&) = delete;
NoCopy& operator=(const NoCopy&) = delete;
};
The static assertion fires just the same, as this live example shows.
If your intent is to guard against people accidentally forgetting to call the NoCopy
copy constructor in their own (forbidden-by-naming) copy constructor, I would suggest this:
namespace
{
struct NotCopyableInitT {};
}
// You can choose whatever stern words you want here.
NotCopyableInitT initNoCopy() { return {}; }
struct NoCopy {
explicit NoCopy(NotCopyableInitT){}
NoCopy(const NoCopy&) = delete;
NoCopy& operator=(const NoCopy&) = delete;
};
If they insist to add copyability where it is forbidden, you force them to spell out their own mistake:
struct Foo2 : mylib::NoCopy {
Foo2() : NoCopy(mylib::initNoCopy()) {}
// Users have to spell out this line in order to get a copy constructor.
// That certainly goes beyond being forgetful.
Foo2(const Foo2&) : NoCopy(mylib::initNoCopy()) {}
};
Demo
For well-behaved users, it's one extra function call in the NoCopy
constructor (which at least a linter would tell you to call explicitly anyway).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With