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

Geo*_*mer 31 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
};
Run Code Online (Sandbox Code Playgroud)

但是,这会在复制构造函数的情况下产生问题,因为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
};
Run Code Online (Sandbox Code Playgroud)

但是,并不总是可以构建您的类,以便默认的构造函数将做正确的事情.

我能看到的另一种方法是使用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
};
Run Code Online (Sandbox Code Playgroud)

除了像罪一样丑陋之外,这种方法的麻烦在于我必须使构造函数成为模板,因为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
};
Run Code Online (Sandbox Code Playgroud)

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

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

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

Col*_*mbo 43

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

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;
};
Run Code Online (Sandbox Code Playgroud)

这种方式的特性is_copy_constructible恰好满足于何处T is_copy_constructible.


Jon*_*ely 9

但是,并不总是可以构建您的类,以便默认的构造函数将做正确的事情.

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

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

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

  C(const C&) = default;
Run Code Online (Sandbox Code Playgroud)

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

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
};
Run Code Online (Sandbox Code Playgroud)

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


Bar*_*rry 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");
    }
};
Run Code Online (Sandbox Code Playgroud)

这将允许复制构造只有在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
}
Run Code Online (Sandbox Code Playgroud)

  • 即使对复制构造函数的调用失败,`std :: is_copy_constructible <Foo <std :: string >> :: value`仍将在您的示例中报告为true. (9认同)

n. *_* m. 6

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() {};
};
Run Code Online (Sandbox Code Playgroud)

This makes a non-eligible template instance have two copy constructors, which makes it not copy constructible.