复制构造函数和转发构造函数之间的冲突

时间:2012-02-15 02:51:24

标签: c++ constructor c++11 clang

此问题基于代码,该代码适用于GCC-4.6,但不适用于另一个使用CLang-3.0的用户,均采用C ++ 0x模式。

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

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

只要MyBase支持该构造签名,T的对象就可以获取任何构造函数参数列表。问题与特殊成员函数有关。

  1. IIUC,构造函数模板取消自动定义的默认构造函数。但是,由于模板可以接受零参数,因此它将充当显式定义的默认构造函数(只要T是默认构造的)。
  2. IIUC,确定类的复制构造策略会忽略构造函数模板。这意味着在这种情况下,MyBase将获得一个自动定义的复制构造函数(只要T可复制),即'T复制构造。
  3. 也适用于移动构造的上一步。
  4. 所以如果我传递MyBase<T> const &作为唯一的构造函数参数,调用哪个构造函数,转发一个或隐式复制一个?

    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
    

    我的用户的问题是将其用作基类。编译器抱怨他的类无法合成自动定义的复制构造函数,因为它找不到与基类的构造函数模板的匹配。它不应该为自己的自动拷贝构造函数调用MyBase自动拷贝构造函数吗? CLang错误地提出了冲突吗?

3 个答案:

答案 0 :(得分:10)

我和Richard Corden在酒吧里,我们之间得出的结论是,这个问题与变量或rvalues无关。在这种情况下,隐式生成的复制构造将MyBase const&作为参数。模板化构造函数将参数类型推导为MyBase&。虽然它不是复制构造函数,但这是一个更好的匹配。

我用于测试的示例代码是:

#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);
}

我需要删除初始化列表的使用,因为clang还不支持。此示例编译除了fv3的初始化失败之外:为MyBase<T>合成的复制构造函数接受MyBase<T> const&,因此传递fv2调用可变参数构造函数将对象转发给基类。

我可能误解了这个问题,但基于d0d1,似乎默认构造函数和复制构造函数都是合成的。但是,这是gcc和clang的最新版本。也就是说,它没有解释为什么没有合成复制构造函数,因为有一个复合构造函数。

要强调此问题与可变参数列表或rvalues无关:以下代码显示了模板化构造函数被调用的问题,尽管它看起来好像是在调用复制构造函数而复制构造函数从不是模板。这实际上是一种令人惊讶的行为,我绝对不知道:

#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);
}

因此,将问题中的可变参数构造函数添加到没有任何其他构造函数的类中会更改行为复制构造函数的工作!就个人而言,我认为这是相当不幸的。这实际上意味着类MyBase也需要使用复制和移动构造函数进行扩充:

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

不幸的是,这似乎不适用于gcc:它抱怨默认的复制构造函数(它声称默认的复制构造函数采用非const引用不能在类定义中定义)。 Clang接受此代码时没有任何投诉。使用非const引用的复制构造函数的定义适用于gcc和clang:

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

答案 1 :(得分:1)

我个人有GCC快照的问题已经有一段时间了。我很难搞清楚发生了什么(如果它被允许的话)但是我得出了与DietmarKühl类似的结论:复制/移动构造器仍在这里,但并不总是通过过载机制来选择分辨率。

我一直用它来解决这个问题一段时间了:

// 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> {};

将它与像你这样的构造函数一起使用看起来像:

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

有些解释是有道理的。 is_related是磨损二元特征的运行,它检查两种类型是否相同,而不管顶级说明符constvolatile&&& )。我们的想法是,将由此特征保护的构造函数是“转换”构造函数,并不是为了处理类类型本身的参数而设计的,而是仅在该参数位于第一个位置时。具有参数的结构,例如(std::allocator_arg_t, MyBase)没问题。

现在我曾经将enable_if_unrelated作为二进制元函数,但是因为在完全转发的可变参数构造函数工作非常方便,我也重新设计它以接受任意数量的参数(尽管它可以设计为接受至少一个参数,我们正在守护的构造函数的类类型。这意味着在我们的情况下,如果没有参数调用构造函数,则不是SFINAE。否则,您需要添加MyBase() = default;声明。

最后,如果构造函数转发到基础,则另一种替代方法是继承该基础的构造函数(即using Base::Base;)。在您的示例中不是这种情况。

答案 2 :(得分:0)

我赞成了Dietmar的回答,因为我完全赞同他。但我想分享一段时间以前我用过的“解决方案”来避免这些问题:

我故意在可变参数构造函数中添加了一个伪参数:

enum fwd_t {fwd};

template<class T>
class wrapper
{
    T m;
public:
    template<class...Args>
    wrapper(fwd_t, Args&&...args)
    : m(std::forward<Args>(args)...)
    {}
};

:::

int main()
{
    wrapper<std::string> w (fwd,"hello world");
}

特别是因为构造函数会接受没有这个伪参数的任何东西,所以通过(命名)“命名”它来明确选择正确的构造函数似乎是合适的。

在您的情况下可能无法实现。但有时你可以逃脱它。