Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Conditionally disabling a copy constructor

Suppose I'm writing a class template C<T> that holds a T value, so C<T> can be copyable only if T is copyable. Normally, when a template might or might not support a certain operation, you just define the operation, and it's up to your callers to avoid calling it when it's not safe:

template <typename T> class C {  private:   T t;   public:   C(const C& rhs);   C(C&& rhs);    // other stuff }; 

However, this creates problems in the case of a copy constructor, because is_copy_constructible<C<T>> will be true even when T is not copyable; the trait can't see that the copy constructor will be ill-formed if it's called. And that's a problem because, for example, vector will sometimes avoid using the move constructor if std::is_copy_constructible is true. How can I fix this?

I believe is_copy_constructible will do the right thing if the constructor is explicitly or implicitly defaulted:

template <typename T> class C {  private:   T t;   public:   C(const C& rhs) = default;   C(C&& rhs) = default;    // other stuff }; 

However, it's not always possible to structure your class so that defaulted constructors will do the right thing.

The other approach I can see is to use SFINAE to conditionally disable the copy constructor:

template <typename T> class C {  private:   T t;   public:   template <typename U = C>   C(typename std::enable_if<std::is_copy_constructible<T>::value,                             const U&>::type rhs);   C(C&& rhs);    // other stuff }; 

Aside from being ugly as sin, the trouble with this approach is that I have to make the constructor a template, because SFINAE only works on templates. By definition, copy constructors are not templates, so the thing I'm disabling/enabling isn't actually the copy constructor, and consequently it won't suppress the copy constructor that's implicitly provided by the compiler.

I can fix this by explicitly deleting the copy constructor:

template <typename T> class C {  private:   T t;   public:   template <typename U = C>   C(typename std::enable_if<std::is_copy_constructible<T>::value,                             const U&>::type rhs);   C(const C&) = delete;   C(C&& rhs);    // other stuff }; 

But that still doesn't prevent the copy constructor from being considered during overload resolution. And that's a problem because all else being equal, an ordinary function will beat a function template in overload resolution, so when you try to copy a C<T>, the ordinary copy constructor gets selected, leading to a build failure even if T is copyable.

The only approach I can find that in principle will work is to omit the copy constructor from the primary template, and provide it in a partial specialization (using more SFINAE trickery to disable it when T is not copyable). However, this is brittle, because it requires me to duplicate the entire definition of C, which creates a major risk that the two copies will fall out of sync. I can mitigate this by having the method bodies share code, but I still have to duplicate the class definitions and the constructor member-init lists, and that's plenty of room for bugs to sneak in. I can mitigate this further by having them both inherit from a common base class, but introducing inheritance can have a variety of unwelcome consequences. Furthermore, public inheritance just seems like the wrong tool for the job when all I'm trying to do is disable one constructor.

Are there any better options that I haven't considered?

like image 688
Geoff Romer Avatar asked Nov 22 '14 01:11

Geoff Romer


People also ask

How do I block a copy constructor?

There are three ways to prevent such an object copy: keeping the copy constructor and assignment operator private, using a special non-copyable mixin, or deleting those special member functions. A class that represents a wrapper stream of a file should not have its instance copied around.

What is implicitly deleted copy constructor?

Deleted implicitly-declared copy constructorT has direct or virtual base class that cannot be copied (has deleted, inaccessible, or ambiguous copy constructors);

Why do we delete the copy constructor?

When to delete copy constructor and assignment operator? Copy constructor (and assignment) should be defined when ever the implicitly generated one violates any class invariant. It should be defined as deleted when it cannot be written in a way that wouldn't have undesirable or surprising behaviour.

What are the disadvantages of copy constructor?

The only disadvantage I can think of to a copy constructor is that some large objects can be expensive to copy (eg. copying a long string involves allocating a large block of memory then copying all the content).


1 Answers

A noteworthy approach is partial specialization of the surrounding class template.

template <typename T,           bool = std::is_copy_constructible<T>::value> struct Foo {     T t;      Foo() { /* ... */ }     Foo(Foo const& other) : t(other.t) { /* ... */ } };  template <typename T> struct Foo<T, false> : Foo<T, true> {     using Foo<T, true>::Foo;      // Now delete the copy constructor for this specialization:     Foo(Foo const&) = delete;      // These definitions adapt to what is provided in Foo<T, true>:     Foo(Foo&&) = default;     Foo& operator=(Foo&&) = default;     Foo& operator=(Foo const&) = default; }; 

This way the trait is_copy_constructible is satisfied exactly where T is_copy_constructible.

like image 150
Columbo Avatar answered Sep 23 '22 05:09

Columbo