A conditional copy constructor

In this post we will try to define a ‘wrapper’ class template that does or does not have a copy constructor depending on whether the wrapped class has it. This will be a good opportunity to explore in depth a couple of advanced C++ features. Note that this is a rather advanced topic and, unless you are writing or maintaining a generic library, you will probably never need this knowledge.

The problem

By a wrapper class I mean something like boost::optional<T>. It wraps a value of type T: it is an ‘almost type T’ with some special features. Additionally, as in the case of optional, our wrapper does not inherit from T, nor does it contain a data member of type T.

Our goal is that for any type T the following holds:

std::is_copy_constructible<T>::value ==
std::is_copy_constructible<Wrapper<T>>::value;

We cannot just specialize template std::is_copy_constructible. According to the C++ Standard, the behavior of the program is undefined if you specialize this template. It is not an ordinary template; it is more like a keyword related to compile-time reflection. This allows the compilers to just check in their database if type T is CopyConstructible without having to perform normal template instantiation. Your job is to make your type behave as intended; the trait’s job is to detect what you have made. These responsibilities had better not be confused. Besides, there are other ways of checking if a type is copy-constructible, and we want to make them work too.

Let’s try…

We will be working with an optional-like class. We said we do not want our wrapper to either derive from T or have a T as its member. This requirement is important. If we could make it a member sub-object, the copy constructor is immediately implicitly generated the way we want it. In our case it is not possible. Much like std::vector, boost::optional stores an object of type T by managing a raw memory buffer and manually calls constructors and destructors of T where appropriate. Its layout can be described as follows:

template <typename T>
class Wrapper
{
private:
  std::aligned_storage_t<sizeof(T), alignof(T)> storage_;
  bool is_initialized_ = false;

  void* storage() {
    return static_cast<void*>(&storage_);
  } 

  const T& get_T() const {
    assert (is_initialized_);

    return *static_cast<const T*>(
      static_cast<const void*>(&storage_));
  }
};

Here, storage_ represents raw memory with the size and layout identical to these of T. Flag is_initialized_ keeps the record of whether the storage now has a constructed object of type T or if it is just rubbish.

The STL way

For the first attempt at ‘conditional’ copy constructor, let’s do what STL containers do. Just define the copy constructor and forward to T’s copy constructor. If the later is not accessible, the former will not compile when used. We can add static_assert inside to make our intent more explicit and a more informative error message:

template <typename T>
Wrapper<T>::Wrapper(const Wrapper& rhs)
{
  static_assert(std::is_copy_constructible<T>::value,
                "Cannot copy-construct Wrapper<T> "
                "for non-copy-constructible T.");

  if (rhs.is_initialized_) {
    ::new (storage()) T(rhs.get_T());
    is_initialized_ = true;      // note the order
  }
}

This ::new construct is called “placement new”. It is used to call the constructor in an already obtained (raw) memory location.

It looks like we have quite cut it. Let’s check it. For the tests throughout this post we will be using these definitions:

struct Resource         // a non-copyable class
{
  Resource(){}
  Resource(const Resource&) = delete;
};

template <typename T>   // variable template (C++14)
constexpr bool CopyCtor = 
  std::is_copy_constructible<T>::value;

Resource represents a non-copy-constructible class. CopyCtor is a variable template. We use it as a bit shorter meta-function for checking if the type is copy-constructible.

Now, the test:

static_assert(!CopyCtor<Resource>, "");          // OK
static_assert(!CopyCtor<Wrapper<Resource>>, ""); // ERR

It doesn’t work: the second assertion fires. When we use the type trait to check if it is copy-constructible, it always says ‘yes’, regardless of T. Here we arrive at an interesting and not so obvious feature of type traits as well as SFINAE-based checks for valid expressions. When the compiler tries to determine whether an expression of interest (copy construction in our case) is “valid”, for the purpose of returning a compile-time Boolean value, or computing some meta-function, it only inspects the signatures of function templates, and does not try to instantiate their bodies (to check if it would work). If we look at the signature only, what we see is this:

template <typename T>
Wrapper<T>::Wrapper(const Wrapper& rhs);

The declaration is not private, or deleted, or SFINAED away. It looks like Wrapper<T> is copy-constructible for any T. Thus, it is not entirely clear what it means for a type to be copy-constructible. It could mean:

  1. I can make copies.
  2. There exists a declaration/signature that would be selected in overload resolution process.

#1 implies #2 but not the other way. To restate the goal of this post. We want to define class Wrapper in such a way that #1 is equivalent to #2. IOW, if it is detectable (with traits) that T is not copy-constructible, it should be also detectable that Wrapper<T> is not copy-constructible.

SFINAE away the copy constructor

Thus, for non-copy-constructible T’s the copy constructor has to disappear. Let’s add an enable_if to the declaration of copy constructor:

template <typename T>
class Wrapper
{
public:
  Wrapper() = default;
  Wrapper(const Wrapper& rhs, 
          std::enable_if_t<CopyCtor<T>, int*> = 0);
};

It is OK for the copy constructor to have additional arguments that have default values. But this declaration is not OK. Now you cannot even default-construct an object of type Wrapper<Resource>. And if we try these assertions:

static_assert( CopyCtor<Wrapper<Resource>>, ""); // ERR
static_assert(!CopyCtor<Wrapper<Resource>>, ""); // ERR

They both fail. This is because the way we used enable_if here is incorrect. The enable_if functionality is based on SFINAE rule, and this rule only applies to function templates and member function templates. In our case we have a “normal” (non-template) member function that happens to be defined inside a class template. enable_if is not going to work here.

Copy construction from template

If enable_if only works with function templates, let’s create a forwarding constructor (template) that can be used for copying instead of the copy constructor:

template <typename T, typename U>
  constexpr bool Same = std::is_same<T, U>::value;

template <typename T>
class Wrapper
{
public:
  Wrapper() = default;

  template <
    typename U,
    std::enable_if_t<
      Same<Wrapper<T>, std::decay_t<U>> && CopyCtor<T>,
      int*
    > = 0
  >
  Wrapper(const U& rhs);
};

Let me explain. This is another way of using enable_if. We have a constructor template with two parameters. The first is ‘normal’: U. The other is not a type but a value parameter and has a default value 0. It ‘enables’ the template in overload resolution only when the argument is Wrapper<T> and T is copy-constructible. Otherwise, the template does not participate in overload resolution. Let’s test it:

static_assert( CopyCtor<Wrapper<int>>, "");      // OK
static_assert(!CopyCtor<Wrapper<Resource>>, ""); // ERR

This doesn’t work: Wrapper<Resource> is recognized as copy-constructible, and worse: you can make copies of Wrapper<Resource>.

This is because the compiler still generates its own copy constructor, which is always available.

Deleted copy constructor

So, let’s declare the copy constructor as deleted, and then rely on our forwarding constructor template:

template <typename T>
class Wrapper
{
public:
  Wrapper() = default;
  Wrapper(const Wrapper&) = delete;

  template <
    typename U,
    std::enable_if_t<
      Same<Wrapper<T>, std::decay_t<U>> && CopyCtor<T>,
      int*
    > = 0
  >
  Wrapper(const U& rhs);
};

Let’s test it:

static_assert( CopyCtor<Wrapper<int>>, "");      // ERR
static_assert(!CopyCtor<Wrapper<Resource>>, ""); // OK

Now, Wrapper<Resource> is non-copy-constructible, as expected; but it is no longer possible to copy-construct Wrapper<int>!

In order to understand what is going here, we have to understand the meaning of this = delete declaration. It does not delete any declaration. Instead it is more-less equivalent to:

template <typename T>
class Wrapper
{
private:
  Wrapper(const Wrapper&) { 
    static_assert(sizeof(T) < 0, "");
  }
};

That is, the function is still declared, and can be selected during the overload resolution; but when selected and used, it causes a compile-time error.

In our case, the deleted copy constructor is still declared and still a better match than the forwarding constructor template. It is always preferred.

Use boost::noncopyable

There are other ways for preventing copy-constructor generation. For instance, boost::noncopyable:

template <typename T>
class Wrapper : boost::noncopyable
{
public:
  Wrapper() = default;

  template <
    typename U,
    std::enable_if_t<
      Same<Wrapper<T>, std::decay_t<U>> && CopyCtor<T>,
      int*
    > = 0
  >
  Wrapper(const U& rhs);
};

Let’s test it:

static_assert( CopyCtor<Wrapper<int>>, "");      // ERR
static_assert(!CopyCtor<Wrapper<Resource>>, ""); // OK

The same problem as before. In order to understand why it is so, we have to abandon the common notion saying that “if the compiler cannot synthesize the copy constructor, it will not generate it.” This is imprecise, and in our case — confusing. If the compiler cannot invoke the copy constructor on all sub-objects because they are inaccessible, it implicitly declares the copy constructor as deleted. And we know the consequences already. So, unless you declare the copy constructor manually, the compiler will always do it for you: in the worse case, it will be a deleted copy constructor.

Conditional base class

Did we run out of options then? No, but before we make the next attempt, let’s go back to the beginning. We said, we assume that Wrapper<T> cannot have T as its member sub-object because otherwise the task would be too easy. Why too easy? Because the compiler automatically generates the copy constructor that does the right thing in the right case:

template <typename T>
class EasyWrapper
{
  T member;
};

Not only is the deep copying performed correctly and in an exception-save way, but also it is deleted whenever T is non-copy-constructible:

static_assert( CopyCtor<EasyWrapper<int>>, "");      // OK
static_assert(!CopyCtor<EasyWrapper<Resource>>, ""); // OK

Note one other thing that is also fairly obvious. Consider the following really trivial class:

struct Copyable{};

It has a number of trivial operations, but we are only interested in copy constructor. Not only is it trivial, but also it is a no-op. Note what happens when we add it as member to our EasyWrapper<T>:

template <typename T>
class EasyWrapper
{
  Copyable _;
  T member;
};

Quite so: nothing. It affects nothing (except perhaps for the class’s size). Now consider another variation:

template <typename T>
class EasyWrapper
{
  boost::noncopyable _;
  T member;
};

Now, the change is significant. EasyWrapper<T> is never copy-constructible, regardless of T. The rule behind this is simple: the synthesized copy constructor is accessible if all sub-objects’ copy constructors are accessible, and conversely: if at least one sub-object has inaccessible copy constructor, the synthesized copy constructor is deleted. Or to put it differently, adding only one non-copy-constructible sub-object to the class can poison the generation of its copy-constructor. This is what we are going to use to solve our problem. Suppose the following is our ‘implementation’ class with the copy constructor that has the correct logic, but is accessible too freely:

template <typename T>
class WrapperImpl
{
private:
  std::aligned_storage_t<sizeof(T), alignof(T)> storage_;
  bool is_initialized_ = false;

  void* storage();
  const T& get_T() const;

public:   
  WrapperImpl() = default;

  WrapperImpl(const WrapperImpl& rhs)  // unconditional
  {
    if (rhs.is_initialized_) {
      ::new (this->storage()) T(rhs.get_T());
      this->is_initialized_ = rhs.is_initialized_;
    } 
  }
};

In the cases where we want the copy-constructor disabled, we could declare the ‘assembling’ class as:

template <typename T>
class Wrapper
{
  boost::noncopyable copyControl_;
  WrapperImpl<T>     impl_;
};

And, for symmetry, in the cases where we want to have the synthesized copy constructor generated, we can type, somewhat artificially:

template <typename T>
class Wrapper
{
  Copyable       copyControl_;
  WrapperImpl<T> impl_;
};

Now, the only thing we need is to combine these two cases. We can achieve this with the standard C++14 type trait conditional_t. Its purpose is exactly to select between two types based on a compile-time condition:

template <typename T> 
using CopyControl = 
  std::conditional_t<
    CopyCtor<T>,          // if T copy-constructible
    Copyable,             // then use base with copy ctor
    boost::noncopyable    // else, use non-copyable base
  >; 

Now, CopyControl<T> denotes either something copyable or something non-copyable, depending on whether T is copy-constructible. We can use it like this:

template <typename T>
class Wrapper
{
  CopyControl<T> copyControl_;
  WrapperImpl<T> impl_;
};

And that’s it. This solves our problem:

static_assert( CopyCtor<Wrapper<int>>, "");      // OK
static_assert(!CopyCtor<Wrapper<Resource>>, ""); // OK

There is just one optimization that we have to apply here. Even though neither Copyable nor boost::noncopyable have any sub-objects inside, they have a non-zero size (as every class in C++), which affects the size of Wrapper<T>. In order to avoid that, rather than using CopyControl<T> as member sub-object, we have to make it a base class sub-object:

template <typename T>
class Wrapper : CopyControl<T>
{
  WrapperImpl<T> impl_;
};

This makes use of the so called Empty Base Optimization.

This is one of the cases where we use inheritance for purposes other than representing the “is-a” relation. I learned this trick from Daniel Krügler’s implementation of optional. You can find it here. By extending this trick, it also covers move-constructor and copy/move assignment.

Concepts Lite

For the end, I wanted to show you how the same problem can be easily addressed with Concepts Lite.

template <typename T>
class Wrapper
{
private:
  // implementation ...

public:   
  Wrapper() = default;

  Wrapper(const Wrapper& rhs)
    requires std::is_copy_constructible<T>::value
  {
    // implementation ...
  }
};

It is one of the problems Concepts Lite were designed to solve. Note that we use ‘requires-clause’ but no concept. It means, “this constructor should participate in overload resolution only if the given condition is true.” Such ‘requires-clause’ is an improved version of enable_if. It is simpler to write, it can be applied to non-template member functions, and (although it is of no relevance to our task) it can be used for ordering rather than disabling function overloads.

Acknowledgements

I am grateful to Tomasz Kamiński for reviewing this post and suggesting a cleaner description in the final solution. I am also grateful to Daniel Krügler for implementing this “conditional copy constructor” and letting me blog about it.

Advertisements
This entry was posted in programming and tagged , , , , , . Bookmark the permalink.

3 Responses to A conditional copy constructor

  1. Paul says:

    You can also solve it by doing this:

    template <
        bool _=true, class=std::enable_if_t<(_ && std::is_copy_constructible<T>())>>
    >
    Wrapper(const Wrapper& rhs);
    
    • “Solve” what? Do you mean solve the main task in this post? If so, I do not think you can. This constructor template does not prevent the implicit declaration of the copy constructor, and in turn, we get:

      static_assert( CopyCtor<Wrapper<int>>, "");      // OK
      static_assert(!CopyCtor<Wrapper<Resource>>, ""); // ERR
      
      • Paul says:

        Yea, you are right, I spoke too soon. Instead we could use a base class to enable or disable the copy constructor, and then use a “too perfect” forwarding constructor:

        template <typename T>
        class Wrapper : std::conditional_t<(std::is_copy_constructible<T>()), 
          copyable, 
          noncopyable
        >
        {
        public:
          Wrapper() = default;
          template<class U, class=std::enable_if_t<(
            std::is_copy_constructible<T>() &&
            std::is_base_of<Wrapper, std::remove_reference_t<U>>()
          )>>
          Wrapper(U&& rhs);
        };
        

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s