SFINAE away a copy constructor
Asked Answered
F

3

16

Under certain conditions, I'd like to SFINAE away the copy constructor and copy assignment operator of a class template. But if I do so, a default copy constructor and a default assignment operator are generated. The SFINAE is done based on tags I pass as class template parameters. The problem is, that SFINAE only works on templates and a copy constructor/assignment operator can't be a template. Does there exist a workaround?

Farber answered 3/4, 2015 at 10:7 Comment(2)
Use a base class that inhibits copy constructor/assignment depending on the template argument.Compendious
Since a template constructor isn't a copy constructor, it also doesn't prevent a copy constructor from being implicitly declared, which will sometimes be preferred over your template constructor, so that seems like a dead end. @stefan's comment seems like it would make for a good answer.Echoechoic
C
14

This solution uses a base class that is conditionally not copyable (by explicitely marking the copy constructor and copy assignment operator as deleted).

template <bool>
struct NoCopy;

template <>
struct NoCopy<true>
{
   // C++11 and later: marking as deleted. Pre-C++11, make the copy stuff private.
   NoCopy(const NoCopy&) = delete;
   NoCopy& operator=(const NoCopy&) = delete;
   protected:
      ~NoCopy() = default; // prevent delete from pointer-to-parent
};

template <>
struct NoCopy<false>
{
   // Copies allowed in this case
   protected:
      ~NoCopy() = default; // prevent delete from pointer-to-parent
};

Example usage:

template <typename Number>
struct Foo : NoCopy<std::is_integral<Number>::value>
{
   Foo() : NoCopy<std::is_integral<Number>::value>{}
   {
   }
};

int main()
{
   Foo<double> a;
   auto b = a; // compiles fine
   Foo<int> f;
   auto g = f; // fails!
}

Note: the destructor of NoCopy is declared protected to avoid virtual inheritance (Thanks for the hint, @Yakk).

Compendious answered 3/4, 2015 at 10:22 Comment(8)
Note: @Walter's implementation of copyable is slightly more elegant (because it only specializes the special case). But obviously both approaches work just fine.Compendious
NoCopy should have a protected destructor in both cases, as it is a class intended to be inherited from, and we don't want to force virtual overhead. Just as a matter of quality of implementation.Thumbnail
@Yakk Thanks for the comment. I didn't know about that language detail yet :)Compendious
also, I removed the <true> from the body of the class. Within the template class itself, the name of the template class can be used without template arguments to refer to the current class type. This removes a lot of clutter. :)Thumbnail
@Yakk, is it possible to provide non-default implementation of Foo<double> copy constructor in this example?Knife
@dmitry yes, it is. Why?Thumbnail
@Yakk, If we provide implementation, Foo<int> won't compile because NoCopy<int> constructor is deleted, will it? Or we can provide partial specialization of Foo<non-int> constructor?Knife
@DmitryJ No, that isn't a problem you can bypass NoCopy<true> if you really want. After all we need to be able to construct Foo, and the copy constructor can use the same parent constructor. NoCopy<true> just disables copying by default.Thumbnail
S
7

The method of deriving from a copyable or non-copyable base is the standard idiom for this type of problem (see also Stefan's comment). One way to implement it is like this:

template<bool> struct copyable
{
protected:
  ~copyable() = default;
};

template<> struct copyable<false> 
{
  copyable(copyable const&) = delete;
  copyable&operator=(copyable const&) = delete;
protected:
  ~copyable() = default;
};

template<bool allow_copy>
class some_class : copyable<allow_copy> { /* ... */ };
Stlaurent answered 3/4, 2015 at 10:21 Comment(9)
Yeah, this is the specialization route.Farber
@Farber no, this is not the specialization route. it uses specialization as an implementation detail, but it doesn't consist of specialization your some_class. And fianlly, copyable should have protected: ~copyable()=default in both cases.Thumbnail
@Yakk Ok, I added the protected destructor. What's the point of that? Is it make copyable a base-only class?Stlaurent
@Stlaurent prevents someone with a pointer to copyable from calling delete on it: any class intended to be derived from should (usually) have a protected destructor, or a virtual destructor, as deleting base-type is one of those hard to track down "oops" cases. In this case, an instance of copyable (that isn't a base class) is clearly nonsense, so there is no harm either.Thumbnail
@Yakk BTW, does the address sanitizer catch such deletions as bugs?Farber
@user "the address sanitizer" -- um, what address sanitizer? Are you thinking of a particular tool?Thumbnail
@user I suspect not at first glance: that appears to just find memory allocation/lifetime type errors (ie, malloc/free), not destructor mismatches (which happens before free).Thumbnail
@Yakk It is more extensive than that. Check man gcc or man clang.Farber
@user so you just told me to look a a many hundred page document (2 of them). I found some docs in there, but they said far less than the previous link. I see no evidence it does "more than that" in the bit I read, am am uninterested at this point in doing further research based on your nebulous claim.Thumbnail
G
3

In C++20, we can use requires-clauses to constrain special member functions:

template <typename T>
class C {
public:
    // ...
    C(const C&) requires std::is_copy_constructible_v<T> // for example
    {
        // ...
    }
    C(C&&) requires std::is_move_constructible_v<T> // for example
    {
        // ...
    }
    // ...
};

A requires-clause does not make the function a function template, so these functions still qualify as special member functions and block the generated default special member functions. You can even have multiple, say, copy constructors, as long as they have different constraints.

Globule answered 23/8, 2019 at 21:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.