35

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?

Geoff Romer
  • 2,358
  • 1
  • 18
  • 19

6 Answers6

48

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.

Columbo
  • 60,038
  • 8
  • 155
  • 203
  • Won't the copy constructor in the primary template use T copy constructor (or default constructor). – Cheers and hth. - Alf Nov 22 '14 at 05:28
  • Answering my own question, no, as long as it's not used it can be anything as longs as it's just syntactically well-formed. It can call non-existing `t.chirp()` for that matter. – Cheers and hth. - Alf Nov 22 '14 at 05:40
  • As noted in the question, this solution has the major drawback that it leads to substantial code duplication. – Geoff Romer Nov 24 '14 at 16:38
  • @GeoffRomer No, that's just plain wrong. Look at the code again. – Columbo Dec 02 '14 at 19:58
  • You're right, I see it now. I'm still concerned about the unintended consequences of inheritance, but I can't point to any specific problems with it. – Geoff Romer Dec 04 '14 at 23:02
  • It's also worth noting that if you want Foo to be move-constructible, you have to explicitly =default it in the specialization. – Geoff Romer Dec 04 '14 at 23:07
  • @GeoffRomer Yes, some special member functions are not implicitly defined as defaulted when you explicitly delete the copy constructor. – Columbo Dec 04 '14 at 23:09
  • @Columbo Yeah, that's well-known, but what's not as obvious is that you don't get the move constructor through inheritance. – Geoff Romer Dec 04 '14 at 23:13
  • @GeoffRomer That's why I added it to the answer :) – Columbo Dec 04 '14 at 23:14
  • @Columbo. I could be wrong but shouldn't the `Foo(Foo&&) = default;` be without argument to match the base class and if `T` is non-copy-constructible, the copy-assignment should be deleted rather than defaulted – Jens Munk Feb 13 '18 at 06:28
9

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

It's usually possible with enough effort.

Delegate the work that can't be done by a defaulted constructor to another member, or wrap the T member in some wrapper that does the copying, or move it into a base class that defines the relevant operations.

Then you can define the copy constructor as:

  C(const C&) = default;

Another way to get the compiler to decide whether the default definition should be deleted or not is via a base class:

template<bool copyable>
struct copyable_characteristic { };

template<>
struct copyable_characteristic<false> {
  copyable_characteristic() = default;
  copyable_characteristic(const copyable_characteristic&) = delete;
};

template <typename T>
class C
: copyable_characteristic<std::is_copy_constructible<T>::value>
{
 public:
  C(const C&) = default;
  C(C&& rhs);

  // other stuff
};

This can be used to delete operations using arbitrary conditions, such as is_nothrow_copy_constructible rather than just a straightforward T is copyable implies C is copyable rule.

Jonathan Wakely
  • 166,810
  • 27
  • 341
  • 521
  • What about a helper type that duplicates the availability of special member functions from a given type (or from the cut of a set of types)? (essentially, combining the traits with the helper class) – dyp Dec 02 '14 at 23:32
7

Update for C++20

In C++20, this is extremely straightforward: you can add a requires to your copy constructor:

template <typename T>
class C {
public:
    C(const C& rhs) requires some_requirement_on<T>
    {
        ...
    }
};

The below solution actually isn't great, because it reports the type as being copyable for all traits - even if it actually isn't.


If you want to conditionally disable your copy constructor, you definitely want it to participate in overload resolution - because you want it to be a loud compile error if you try to copy it.

And to do that, all you need is static_assert:

template <typename T>
class C {
public:
    C(const C& rhs) {
        static_assert(some_requirement_on<T>::value, 
            "copying not supported for T");
    }
};

This will allow copy construction only if some_requirement_on<T> is true, and if it's false, you can still use the rest of the class... just not copy construction. And if you do, you'll get a compile error pointing to this line.

Here's a simple example:

template <typename T>
struct Foo
{
    Foo() { }

    Foo(const Foo& ) {
        static_assert(std::is_integral<T>::value, "");
    }

    void print() {
        std::cout << "Hi" << std::endl;
    }
};

int main() {
    Foo<int> f;
    Foo<int> g(f); // OK, satisfies our condition
    g.print();     // prints Hi

    Foo<std::string> h;
    //Foo<std::string> j(h); // this line will not compile
    h.print(); // prints Hi
}
Barry
  • 286,269
  • 29
  • 621
  • 977
  • 11
    `std::is_copy_constructible>::value` will still report as true in your example, even though a call to the copy constructor will fail. – Benjamin Lindley Nov 22 '14 at 02:01
  • The above should be easily solvable by specializing `std::is_copy_constructible` for this specific class – Raven Jan 12 '23 at 15:18
  • @Raven You're not allowed to do that. And even if you were, that's insufficient anyway - since then `std::is_constructible` would probably give the wrong answer. – Barry Jan 12 '23 at 18:41
  • According to https://stackoverflow.com/a/25992877/3907364 adding specializations for user-defined types should be allowed, unless explicitly prohibited. However, according to [cppreference](https://en.cppreference.com/w/cpp/types/is_copy_constructible) this is exactly the case for these traits. @Barry thanks for pointing this out! I was not aware that there were exceptions to what one is allowed to specialize. – Raven Jan 12 '23 at 18:51
  • @Raven It doesn't say it's allowed unless explicitly prohibited, it says prohibited unless explicitly allowed. – Barry Jan 12 '23 at 22:21
6
template <typename T>
class variant {
    struct moo {};
public:
  variant(const variant& ) = default;
  variant(std::conditional_t<!std::is_copy_constructible<T>::value,
                             const variant&, moo>,
          moo=moo());
  variant() {};
};

This makes a non-eligible template instance have two copy constructors, which makes it not copy constructible.

n. m. could be an AI
  • 112,515
  • 14
  • 128
  • 243
3

This is a bit of a trick, but it works.

template<bool b,class T>
struct block_if_helper{
  using type=T;
};
template<class T>
struct block_if_helper<true, T>{
  class type{
    type()=delete;
  };
};
template<bool b,classT>
using block_if=typename block_if_helper<b,T>::type;
template<bool b,classT>
using block_unless=typename block_if_helper<!b,T>::type;

now we create a method that is your copy ctor ... maybe.

template<class X>
struct example {
  enum { can_copy = std::is_same<X,int>{} };

  example( block_unless<can_copy, example>const& o ); // implement this as if `o` was an `example`
  // = default not allowed
  example( block_if<can_copy, example>const& )=delete;
};

and now the =default is the copy ctor if and only if can_copy, and the =delete of not. The stub type that it is otherwise cannot be created.

I find this technique useful for general method disabling on compilers that do not support the default template argument feature, or for methods (like virtual or special) that cannot be templates.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • I think this won't work: if `can_copy` is false, the program will be ill-formed because you can only `=default` special member functions. SFINAE doesn't apply in this case because the function isn't a template, so this just results in a hard error. – Geoff Romer Dec 08 '14 at 19:14
  • @GeoffRomer good point: so we lose `=default`, that side has to be explicitly written. :/ – Yakk - Adam Nevraumont Dec 08 '14 at 19:15
2

C::C(C const& rhs, std::enable_if<true, int>::type dummy = 0) is also a copy ctor because the second argument has a default value.

MSalters
  • 173,980
  • 10
  • 155
  • 350
  • I don't quite see how you can use this construct to *conditionally* enable the copy ctor. – dyp Dec 02 '14 at 20:01
  • Well, you obviously wouldn't use `true` - it's only unconditionally enabled because I used `true` for brevity. – MSalters Dec 02 '14 at 22:41
  • 1
    When I follow the "naïve" approach, I'd try to make the condition dependent on the template parameter of the class template. That doesn't work because it produces a hard error (when instantiating the class definition). Then, I'd try making that constructor a template to use a template parameter of the constructor template. That wouldn't work either because it's not a copy ctor any more. – dyp Dec 02 '14 at 23:08
  • SFINAE only applies to function templates. This is not a function template, but a regular function in a class template, in which case substitution failure *is* an error. – oisyn Nov 14 '18 at 01:14