【问题标题】:Conditionally disabling a copy constructor有条件地禁用复制构造函数
【发布时间】:2015-01-20 06:58:46
【问题描述】:

假设我正在编写一个类模板C<T>,它包含一个T 值,那么只有当T 是可复制的时,C<T> 才能是可复制的。通常,当模板可能支持或可能不支持某个操作时,您只需定义该操作,并由调用者在不安全时避免调用它:

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

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

  // other stuff
};

但是,这在复制构造函数的情况下会产生问题,因为即使T 不可复制,is_copy_constructible&lt;C&lt;T&gt;&gt; 也会为真;如果调用该特征,则该特征看不到复制构造函数的格式错误。 这是一个问题,例如,如果std::is_copy_constructible 为真,vector 有时会避免使用移动构造函数。我该如何解决这个问题?

如果构造函数显式或隐式默认,我相信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&lt;T&gt; 时,普通的复制构造函数会被选中,即使 T 也会导致构建失败是可复制的。

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

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

【问题讨论】:

    标签: c++ templates constructor sfinae


    【解决方案1】:

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

    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 的位置得到满足。

    【讨论】:

    • 主模板中的复制构造函数不会使用T复制构造函数(或默认构造函数)。
    • 回答我自己的问题,不,只要不使用它,只要它在语法上格式正确,它就可以是任何东西。它可以调用不存在的t.chirp()
    • 如问题中所述,此解决方案的主要缺点是会导致大量代码重复。
    • @GeoffRomer 不,这完全是错误的。再看代码。
    • 你说得对,我现在明白了。我仍然担心继承的意外后果,但我无法指出任何具体问题。
    【解决方案2】:

    但是,并非总是可以构造您的类,以便默认构造函数执行正确的操作。

    只要付出足够的努力通常是可能的。

    将默认构造函数无法完成的工作委托给另一个成员,或将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 是可复制的规则。

    【讨论】:

    • 从给定类型(或从一组类型的剪切)复制特殊成员函数的可用性的辅助类型怎么样? (本质上,将特征与辅助类结合起来)
    【解决方案3】:

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

    为此,您只需要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&lt;T&gt; 为真时才允许复制构造,如果为假,您仍然可以使用该类的其余部分...只是不要复制构造。如果你这样做了,你会得到一个指向这一行的编译错误。

    这是一个简单的例子:

    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
    }
    

    【讨论】:

    • std::is_copy_constructible&lt;Foo&lt;std::string&gt;&gt;::value 在您的示例中仍将报告为 true,即使对复制构造函数的调用将失败。
    【解决方案4】:
    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】:

      这有点小技巧,但确实有效。

      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 是复制ctor 当且仅当can_copy=delete 不是。否则无法创建存根类型。

      我发现这种技术对于在不支持默认模板参数功能的编译器上禁用通用方法或对于不能为 templates 的方法(如 virtual 或特殊方法)很有用。

      【讨论】:

      • 我认为这行不通:如果can_copy 为假,程序将不正确,因为您只能使用=default 特殊成员函数。 SFINAE 在这种情况下不适用,因为该函数不是模板,所以这只会导致硬错误。
      • @GeoffRomer 好点:所以我们失去了=default,那一面必须明确写出来。 ://
      【解决方案6】:

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

      【讨论】:

      • 我不太明白如何使用这个构造来有条件地启用复制ctor。
      • 好吧,你显然不会使用true——它只是无条件启用,因为我为了简洁起见使用了true
      • 当我遵循“幼稚”的方法时,我会尝试使条件依赖于类模板的模板参数。这不起作用,因为它会产生一个硬错误(在实例化类定义时)。然后,我会尝试将该构造函数作为模板来使用构造函数模板的模板参数。这也行不通,因为它不再是复制 ctor。
      • SFINAE 仅适用于函数模板。这不是函数模板,而是类模板中的常规函数​​,在这种情况下替换失败错误。
      猜你喜欢
      • 2011-08-29
      • 2011-07-18
      • 2013-05-22
      • 2017-07-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多