Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Trivial compile time check for no copy constructor

Tags:

c++

c++11

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?

like image 388
arynaq Avatar asked Oct 15 '18 10:10

arynaq


People also ask

What happens if there is no copy constructor?

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.

What does trivially copyable mean?

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.

What does trivial mean in C++?

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.


2 Answers

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.

like image 163
StoryTeller - Unslander Monica Avatar answered Sep 28 '22 09:09

StoryTeller - Unslander Monica


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).

like image 31
Max Langhof Avatar answered Sep 28 '22 09:09

Max Langhof