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

chhkpiq4  于 2023-01-15  发布在  其他
关注(0)|答案(6)|浏览(133)

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

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

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

  // other stuff
};

然而,这在复制构造函数的情况下产生了问题,因为即使T不可复制,is_copy_constructible<C<T>>也将为真;trait无法看到复制构造函数在被调用时是病态的,这是一个问题,因为,例如,如果std::is_copy_constructible为真,vector有时会避免使用move构造函数,我该如何修复这个问题?
我相信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是可复制的。
我能找到的原则上可行的唯一方法是从主模板中省略复制构造函数,并在部分专用化中提供它(使用更多的SFINAE技巧在T不可复制时禁用它)然而,这是脆弱的,因为它需要我复制C的整个定义,这就产生了两个副本不同步的主要风险。我可以通过让方法体共享代码来减轻这种风险,但我仍然必须复制类定义和构造函数成员初始化列表,我可以通过让它们都从一个公共基类继承来进一步减轻这个问题,但是引入继承可能会有各种不受欢迎的后果。此外,当我试图做的只是禁用一个构造函数时,公共继承似乎是一个错误的工具。
有什么更好的选择我没有考虑过吗?

l5tcr1uw

l5tcr1uw1#

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

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恰好在Tis_copy_constructible处得到满足。

0qx6xfy6

0qx6xfy62#

然而,并不总是能够构造类,使默认构造函数执行正确的操作。
通常只要付出足够的努力就能做到。
将默认构造函数无法完成的工作委托给另一个成员,或者将T成员 Package 在执行复制的 Package 器中,或者将其移动到定义相关操作的基类中。
然后可以将复制构造函数定义为:

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是可复制的规则。

2guxujil

2guxujil3#

针对C++20的更新

在C++20中,这是非常直接的:你可以在复制构造函数中添加一个requires

template <typename T>
class C {
public:
    C(const C& rhs) requires some_requirement_on<T>
    {
        ...
    }
};

下面的解决方案实际上并不是很好,因为它报告类型对于所有traits都是可复制的--即使它实际上不是。
如果你想有条件地禁用你的复制构造函数,你肯定希望它参与重载解析--因为如果你试图复制它,你希望它成为一个响亮的编译错误。
要做到这一点,您只需要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
}
tct7dpnv

tct7dpnv4#

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

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

vwoqyblh

vwoqyblh5#

这是一个小把戏,但它的工作。

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;

现在我们创建一个方法,它可能是你的复制向量。

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不是,否则它是的存根类型不能被创建。
我发现这种技术对于在不支持默认模板参数特性的编译器上禁用常规方法,或者对于不能是template的方法(如virtual或special)很有用。

2ledvvac

2ledvvac6#

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

相关问题