【问题标题】:Only overload operator if template argument does如果模板参数确实重载运算符
【发布时间】:2015-04-24 19:31:31
【问题描述】:

给定一个带有单个模板参数 T 的模板类 A,是否可以仅重载 A 中可用于类型 T 的运算符?例如:

template <typename T>
class A
{
public:
    #if hasOperator(T, +=)
    T& operator +=(const T &rhs)
    {
        mValue += rhs;
        return mValue;
    }
    #endif

private:
    T mValue;
}


int main()
{
    A<int> a;
    a += 8; //+= will forward to the += for the int

    struct Test {  /*no operators defined*/ };
    A<Test> b; //+= is not implemented since Test does not implement +=
}

我正在编写一个需要与模板类型完全一样的通用包装类。因此,如果 T 有运算符 +=,A 将(在编译时)相应地重载 +=。是的,我可以继续在 A 中实现每个运算符,但是当 T 没有特定运算符时,编译器会出错。起初我认为模板专业化可能是答案,但这需要对每种类型进行专业化。虽然这可能有效并且需要大量输入,但它不会,因为 A 需要处理任何类型(不仅仅是专门的类型)。

【问题讨论】:

  • 看起来你在找enable_if
  • @PiotrS。我急于回答时甚至没有想到这一点:) 但是在不太可能的情况下,OP 想要显式实例化不实现 operator+ 的类型的模板,上面的代码将失败。
  • @DanWatkins 但这只是一个通知,您可能希望将此运算符完全排除在重载集之外

标签: c++ operator-overloading c++14


【解决方案1】:

除非T 定义operator+,否则使用表达式SFINAE 将operator+ 从重载解决方案集中删除

template <typename T>
class A
{
private:
    T mValue;
public:
    template<typename U=T>
    auto operator +=(const U &rhs)
        -> decltype(mValue += rhs)
    {
        mValue += rhs;
        return mValue;
    }
};

Live demo

【讨论】:

  • 我知道 OP 弄错了,但 operator+= 应该始终返回对 this 的引用。
  • @o11c 是的,任何事情都肯定令人惊讶,但我会保持原样,因为那是 OP 发布的内容。
  • 请注意,这可以说是 UB 的复杂方式。也就是说,标准要求模板函数具有有效的实例化,或者程序格式错误,不需要诊断。它还要求不实例化模板类的未调用方法。两者之间存在歧义。
  • @Yakk 我可能错了,但关于你的第一条评论,只有当 any 的模板没有有效的实例化时,程序才不是格式错误的一组类型?在这种情况下,如果T 实现了操作符,它就可以被实例化。我对{} 的事情有点惊讶……我预计它也会失败,但似乎无法通过find a way 来实现。
  • @Yakk += 不需要成为会员。所以A&lt;noinc&gt;::operator+=&lt;hacky_type&gt; 可能是一些hacky_type 的有效实例化,如果有noinc&amp; operator+=(noinc&amp;, hacky_type);
【解决方案2】:

我将给出 3 个降低复杂性和实用性的解决方案。最后一种解决方案是最简单的,也是最不复杂的。


一个小的,如果有用的话,元编程库:

template<class...>struct types{using type=types;};
namespace details {
  template<template<class...>class Z, class types, class=void>
  struct can_apply : std::false_type {};
  template<template<class...>class Z, class...Ts>
  struct can_apply<Z,types<Ts...>,std::void_t<Z<Ts...>>> :
    std::true_type
  {};
}
template<template<class...>class Z, class...Ts>
using can_apply = details::can_apply<Z,types<Ts...>>;

+= 结果的特征:

template<class Lhs, class Rhs>
using plus_equal_result = decltype(std::declval<Lhs>()+=std::declval<Rhs>());

template<class Lhs, class Rhs>
using can_plus_equal = can_apply< plus_equal_result, Lhs, Rhs >;
template<class T>
using can_self_plus_equal = can_plus_equal< T&, T const& >;

这为我们提供了一些不错的特征,根据 += 是否有效返回真类型或假类型。

template<class A, class T, bool b = can_self_plus_equal<T>{}>
struct A_maybe_plus_equal {};
template<class A, class T>
struct A_maybe_plus_equal<A, T, true> {
  A& self() { return *static_cast<A*>(this); }
  A& operator+=( T && t )
  {
    self().mResult += std::move(t);
    return self();
  }
  template<class U>
  std::enable_if_t<can_plus_equal<T&,U>{},A&> operator+=( U && u )
  {
    self().mResult += std::forward<U>(u);
    return self();
  }
};

这给了我们一个+= 如果我们通过了 true。

template <class T>
class A:
  public A_maybe_plus_equal<A<T>, T>
{
  friend class A_maybe_plus_equal<A<T>, T>;
public:
  // nothing needed
private:
  T mValue;
};

当且仅当T&amp; += T const&amp; 是一个有效的表达式时,它会给你一个+= 重载,它在右侧采用const T&amp;T&amp;&amp;U&amp;&amp;

这是“完美”的解决方案,但很复杂。

请注意,每个运算符都可以单独完成,因此您不会出现专业化的组合爆炸。


现在,有一个更简单的选择。它的缺点是它不支持右侧基于{} 的构造,并且在某些标准读数下它是非法的。

不过,它仍然对 SFINAE 友好:

template <typename T>
class A {
public:
  template<class U>
  auto operator +=(U&&rhs)
  -> decltype( (std::declval<T&>()+=std::declval<U&&>()),void(),A& )
  // or std::enable_if_t<can_plus_equal<T&,U>{},A&>
  {
    mValue += std::forward<U>(rhs);
    return *this;
  }
private:
  T mValue;
};

这可以折叠到上面的选项中,并给出{} 和完美的转发语法。我发现如果你有一个 template 完美转发器,T const&amp; 可以被删除。

这在技术上是未定义行为的原因是标准要求所有模板函数至少有一组参数,可以使其主体能够编译。在给定的类实例中,上述模板+= 可能没有这样的类型参数集,这会使您的程序格式错误,无需诊断(即UB)。

还有一条规则是模板类的成员函数除非被调用,否则不会被实例化。有人争辩说,这条规则取代了我在上一段中提到的规则。

另一个论点是该方法可能是合法的,只要有一些模板参数混合到封闭类和模板方法本身,导致它可以实例化。我猜这是标准委员会的意图,但我不知道如何阅读标准来获得这个结果。

这个论点也适用于答案#1 中的plus_equal 函数。该实现不需要那么简单。此外,#1 提供了基于{}+= 语法,这是使用它的实际原因。这种担心——程序在技术上是错误的——是学术上的,因为我使用的所有编译器都没有这个结构的问题。


上面的第三段给了我们最后的选择。什么都不做。

template <typename T>
class A {
public:
  A& operator +=(const T &rhs) {
    mValue += rhs;
    return *this;
  }
private:
  T mValue;
};

这意味着您不能 SFINAE 测试 += 不起作用,但只要您不调用 += 它“起作用”。例如,vectoroperator&lt; 就是这样工作的。这是一个“质量”较低的解决方案,标准库中的这种情况往往会随着时间的推移而得到修复。

但是,作为第一次通过,最后的选择通常是最好的。只有当您期望 SFINAE 要求时,上述箍筋才值得。


最终,C++1z 正在引入概念。我相信概念会让这个问题变得更容易,因为基于封闭类的类型参数从考虑中消除重载是std 中的一个长期问题。

【讨论】:

    【解决方案3】:

    您实际上不必做任何事情。模板类的各个成员函数在使用之前不会被实例化。你说:

    但是当T没有特定的操作符时编译器会出错。

    但是当A&lt;T&gt; 没有出错时,这不是比出错更清楚吗?如果你有:

    template <typename T>
    class A
    {
    public:
        A& operator +=(const T &rhs)
        {
            mValue += rhs;
            return *this;
        }
    
        A& operator-=(const T &rhs)
        {
            mValue -= rhs;
            return *this;
        }
    
        // etc
    
    private:
        T mValue;
    };
    

    这样就可以了:

    int main() {
        A<int> a;
        a += 8; //+= will forward to the += for the int
    
        struct Test {
            Test& operator-=(const Test& ) { return *this; }
        };
    
        A<Test> b;
        b -= Test{};   // totally fine
        b += Test{};   // error: no match for += 
                       // (operand types are 'main()::Test' and 'const main()::Test')
    }
    

    【讨论】:

      猜你喜欢
      • 2021-02-17
      • 1970-01-01
      • 2020-02-07
      • 1970-01-01
      • 2015-09-15
      • 1970-01-01
      • 1970-01-01
      • 2022-10-01
      • 1970-01-01
      相关资源
      最近更新 更多