有条件地禁用复制构造函数

时间:2014-11-22 01:35:08

标签: c++ templates constructor sfinae

假设我正在编写一个包含C<T>值的类模板T,因此C<T>只有在T可复制的情况下才能复制。通常情况下,当模板可能支持或可能不支持某项操作时,您只需定义操作,然后由您的来电者决定是否在不安全时调用它:

template <typename T>
class C {
 private:
  T t;

 public:
  C(const C& rhs);
  C(C&& rhs);

  // other stuff
};

但是,这会在复制构造函数的情况下产生问题,因为即使is_copy_constructible<C<T>>不可复制,T也会为真;如果调用了复制构造函数,那么该复制构造函数将会形成错误。并且那是一个问题,因为,例如,如果vector为真,std::is_copy_constructible有时会避免使用移动构造函数。我该如何解决这个问题?

如果构造函数显式或隐式默认,我相信is_copy_constructible会做正确的事情:

template <typename T>
class C {
 private:
  T t;

 public:
  C(const C& rhs) = default;
  C(C&& rhs) = default;

  // other stuff
};

然而,并不总是可以构建你的类,以便默认的构造函数能做正确的事。

我能看到的另一种方法是使用SFINAE来有条件地禁用复制构造函数:

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

除了像罪一样丑陋之外,这种方法的麻烦在于我必须使构造函数成为模板,因为SFINAE只适用于模板。根据定义,复制构造函数不是模板,因此我禁用/启用的东西实际上不是复制构造函数,因此它不会抑制由...隐式提供的复制构造函数。编译器。

我可以通过显式删除复制构造函数来解决这个问题:

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

但是这仍然不能防止在重载解析期间考虑复制构造函数。这是一个问题因为其他条件相同,普通函数会在重载决策中击败函数模板,因此当您尝试复制C<T>时,普通复制构造函数会被选中,从而导致构建失败即使T是可复制的。

我能找到的唯一方法原则上是工作是从主模板中省略复制构造函数,并以部分特化方式提供它(当T不可复制时,使用更多的SFINAE技巧来禁用它)。但是,这很脆弱,因为它要求我复制C的整个定义,这会产生两个副本不同步的主要风险。我可以通过让方法体共享代码来缓解这种情况,但我仍然需要复制类定义和构造函数成员初始化列表,而且还有足够的空间让错误潜入。我可以通过以下方式进一步缓解让它们都从公共基类继承,但引入继承可能会产生各种不受欢迎的后果。此外,当我尝试做的只是禁用一个构造函数时,公共继承似乎是错误的工具。

有没有更好的选择我还没考虑过?

6 个答案:

答案 0 :(得分:41)

值得注意的方法是对周围类模板进行部分特化。

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

这样,特性is_copy_constructible完全符合T is_copy_constructible的位置。

答案 1 :(得分:9)

  

然而,并不总是可以构建你的类,以便默认的构造函数能做正确的事。

通常可以付出足够的努力。

将默认构造函数可以完成的工作委托给另一个成员,或者将T成员包装在执行复制的某个包装器中,或者将其移动到定义相关的基类中操作

然后您可以将复制构造函数定义为:

  C(const C&) = default;

让编译器决定是否应该删除默认定义的另一种方法是通过基类:

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

这可用于删除使用任意条件的操作,例如is_nothrow_copy_constructible,而不仅仅是简单的T是可复制的,暗示C是可复制的规则。

答案 2 :(得分:6)

如果要有条件地禁用复制构造函数,您肯定希望它参与重载解析 - 因为如果您尝试复制它,则希望它是一个响亮的编译错误。

要做到这一点,你需要的只是static_assert

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

只有当some_requirement_on<T>为真时才允许复制构造,如果它为假,你仍然可以使用该类的其余部分......只是不复制构造。如果这样做,您将收到指向此行的编译错误。

这是一个简单的例子:

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
}

答案 3 :(得分:3)

这是一个技巧,但它确实有效。

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;

现在我们创建一个方法,你的副本ctor ...也许。

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

现在,=default是版权所有者,当且仅当can_copy=delete不是。virtual。否则无法创建存根类型。

我发现这种技术对于不支持默认模板参数功能的编译器的常规方法禁用,或者对于不能template s的方法(如{{1}}或特殊)非常有用。

答案 4 :(得分:3)

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() {};
};

这使得不合格的模板实例具有两个复制构造函数,这使得它不能复制构造。

答案 5 :(得分:2)

C::C(C const& rhs, std::enable_if<true, int>::type dummy = 0)也是一个复制ctor,因为第二个参数有一个默认值。