仅当模板参数有重载运算符时

Only overload operator if template argument does

本文关键字:重载 运算符 参数      更新时间:2023-10-16

给定一个模板类 A 具有单个模板参数 T,是否可以只重载 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 需要处理任何类型(而不仅仅是专用类型)。

使用表达式 SFINAE 从重载分辨率集中删除operator+,除非T定义了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;
}
};

现场演示

我将给出 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();
}
};

这给了我们一个+=,我们通过了真实。

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& += T const&是有效的表达式时,它会在右侧const T&T&&U&&

这是"完美"的解决方案,但它很复杂。

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


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

然而,它仍然对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&可以放弃。

这在技术上是未定义行为的原因是,标准要求所有模板函数至少具有一组参数,这些参数将使其主体能够编译。 在给定的类实例中,上述模板+=可能没有这样的类型参数集,这使得程序格式不正确,不需要诊断(即 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<的工作方式。 这是一个较低的"质量"解决方案,标准库中的这种情况往往会随着时间的推移而修复。

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


最终,C++1z正在引入概念。 我相信概念将使这个问题变得容易得多,因为根据封闭类的类型参数从考虑中消除重载是std中长期存在的问题。

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

但是当编译器没有特定的运算符时,编译器会出错T

但是,这难道不是比A<T>没有出错更清楚吗?如果您有:

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')
}