Conflict between copy constructor and forwarding constructor

This problem is based on code that works for me on GCC-4.6 but not for another user with CLang-3.0, both in C++0x mode.

template <typename T>
struct MyBase
{
//protected:
    T  m;

    template <typename Args...>
    MyBase( Args&& ...x ) : m( std::forward<Args>(x)... ) {}
};

An object of MyBase can take any list of constructor arguments, as long as T supports that construction signature. The problem has to do with the special-member functions.

  1. IIUC, the constructor template cancels the automatically-defined default constructor. However, since the template can accept zero arguments, it will act as an explicitly-defined default constructor (as long as T is default-constructible).
  2. IIUC, determination of a class' copy-construction policy ignores constructor templates. That means in this case that MyBase will gain an automatically-defined copy constructor (as long as T is copyable) that'll channel T copy-construction.
  3. Apply the previous step for move-construction too.

So if I pass a MyBase<T> const & as the sole constructor argument, which constructor gets called, the forwarding one or the implicit copying one?

typedef std::vector<Int>  int_vector;
typedef MyBase<int_vector>   VB_type;

int_vector  a{ 1, 3, 5 };
VB_type     b{ a };
VB_type     c{ b };  // which constructor gets called

My user's problem was using this in as a base class. The compiler complained that his class couldn't synthesize an automatically-defined copy constructor, because it couldn't find a match with the base class' constructor template. Shouldn't it be calling MyBase automatic copy-constructor for its own automatic copy-constructor? Is CLang in error for coming up with a conflict?


Solution 1:

I'm just in the bar with Richard Corden and between us we concluded that the problem has nothing to do with variadic or rvalues. The implicitly generated copy construct in this case takes a MyBase const& as argument. The templated constructor deduced the argument type as MyBase&. This is a better match which is called although it isn't a copy constructor.

The example code I used for testing is this:

#include <utility>
#include <vector>i

template <typename T>
struct MyBase
{
    template <typename... S> MyBase(S&&... args):
        m(std::forward<S>(args)...)
    {
    }
    T m;
};

struct Derived: MyBase<std::vector<int> >
{
};

int main()
{
    std::vector<int>                vec(3, 1);
    MyBase<std::vector<int> > const fv1{ vec };
    MyBase<std::vector<int> >       fv2{ fv1 };
    MyBase<std::vector<int> >       fv3{ fv2 }; // ERROR!

    Derived d0;
    Derived d1(d0);
}

I needed to remove the use of initializer lists because this isn't supported by clang, yet. This example compiles except for the initialization of fv3 which fails: the copy constructor synthesized for MyBase<T> takes a MyBase<T> const& and thus passing fv2 calls the variadic constructor forwarding the object to the base class.

I may have misunderstood the question but based on d0 and d1 it seems that both a default constructor and a copy constructor is synthesized. However, this is with pretty up to date versions of gcc and clang. That is, it doesn't explain why no copy constructor is synthesized because there is one synthesized.

To emphasize that this problem has nothing to do with variadic argument lists or rvalues: the following code shows the problem that the templated constructor is called although it looks as if a copy constructor is called and copy constructors are never templates. This is actually somewhat surprising behavior which I was definitely unaware of:

#include <iostream>
struct MyBase
{
    MyBase() {}
    template <typename T> MyBase(T&) { std::cout << "template\n"; }
};

int main()
{
    MyBase f0;
    MyBase f1(const_cast<MyBase const&>(f0));
    MyBase f2(f0);
}

As a result, adding a variadic constructor as in the question to a class which doesn't have any other constructors changes the behavior copy constructors work! Personally, I think this is rather unfortunate. This effectively means that the class MyBase needs to be augmented with copy and move constructors as well:

    MyBase(MyBase const&) = default;
    MyBase(MyBase&) = default;
    MyBase(MyBase&&) = default;

Unfortunately, this doesn't seem to work with gcc: it complains about the defaulted copy constructors (it claims the defaulted copy constructor taking a non-const reference can't be defined in the class definition). Clang accepts this code without any complaints. Using a definition of the copy constructor taking a non-const reference works with both gcc and clang:

template <typename T> MyBase<T>::MyBase(MyBase<T>&) = default;

Solution 2:

I've personally had the problem with GCC snapshots for quite some time now. I've had trouble figuring out what was going on (and if it was allowed at all) but I came to a similar conclusion as Dietmar Kühl: the copy/move constructors are still here, but are not always preferred through the mechanics of overload resolution.

I've been using this to get around the problem for some time now:

// I don't use std::decay on purpose but it shouldn't matter
template<typename T, typename U>
using is_related = std::is_same<
    typename std::remove_cv<typename std::remove_reference<T>::type>::type
    , typename std::remove_cv<typename std::remove_reference<U>::type>::type
>;

template<typename... T>
struct enable_if_unrelated: std::enable_if<true> {};

template<typename T, typename U, typename... Us>
struct enable_if_unrelated
: std::enable_if<!is_related<T, U>::value> {};

Using it with a constructor like yours would look like:

template<
    typename... Args
    , typename = typename enable_if_unrelated<MyBase, Args...>::type
>
MyBase(Args&&... args);

Some explanations are in order. is_related is a run off the mill binary trait that checks that two types are identical regardless of top-level specifiers (const, volatile, &, &&). The idea is that the constructors that will be guarded by this trait are 'converting' constructors and are not designed to deal with parameters of the class type itself, but only if that parameter is in the first position. A construction with parameters e.g. (std::allocator_arg_t, MyBase) would be fine.

Now I used to have enable_if_unrelated as a binary metafunction, too, but since it's very convenient to have perfectly-forwarding variadic constructors work in the nullary case too I redesigned it to accept any number of arguments (although it could be designed to accept at least one argument, the class type of the constructor we're guarding). This means that in our case if the constructor is called with no argument it is not SFINAE'd out. Otherwise, you'd need to add a MyBase() = default; declaration.

Finally, if the constructor is forwarding to a base another alternative is to inherit the constructor of that base instead (i.e. using Base::Base;). This is not the case in your example.