在SFINAE模板中如何避免这句话是假的?

时间:2013-04-09 20:52:44

标签: c++ templates c++11 operator-overloading sfinae

所以我想写一个自动!=

template<typename U, typename T>
bool operator!=(U&& u, T&& t) {
  return !( std::forward<U>(u) == std::forward<T>(t) );
}

但这是不礼貌的 1 。所以我写了

// T() == U() is valid?
template<typename T, typename U, typename=void>
struct can_equal:std::false_type {};

template<typename T, typename U>
struct can_equal<
   T,
   U,
   typename std::enable_if<
      std::is_convertible<
         decltype( std::declval<T>() == std::declval<U>() ),
         bool
      >::value
   >::type
>: std::true_type {};

这是一个类型特征类,它表示“是t == u有效代码,它返回一个可转换为bool的类型”。

所以我改进了我的!=

template<typename U, typename T,
  typename=typename std::enable_if<can_equal<T,U>::value>::type
>
bool operator!=(U&& u, T&& t) {
  return !( std::forward<U>(u) == std::forward<T>(t) );
}

现在只有==存在才有效覆盖。可悲的是,它有点贪心:

struct test {
};
bool operator==(const test&, const test&);
bool operator!=(const test&, const test&);

因为它会在每个test() != test()而不是上面调用!=时咆哮。我认为这是不可取的 - 我宁愿拨打一个明确的!=而不是自动转发给==并否定。

所以,我写了这个特质类:

template<typename T, typename U,typename=void>
struct can_not_equal // ... basically the same as can_equal, omitted

测试T != U是否有效。

然后我们按如下方式扩充!=

template<typename U, typename T,
  typename=typename std::enable_if<
    can_equal<T,U>::value
    && !can_not_equal<T,U>::value
  >::type
>
bool operator!=(U&& u, T&& t) {
  return !( std::forward<U>(u) == std::forward<T>(t) );
}

,如果你解析它,说“这句话是假的” - operator!=T之间存在U iff operator!=在{{1}之间不存在}和T

毫不奇怪,每个编译器在测试时都会测试段错误。 (clang 3.2,gcc 4.8 4.7.2 intel 13.0.1)。 我怀疑我正在做的事情是非法的,但我很乐意看到标准参考。(编辑:我正在做的是非法的,因为它会导致无限的递归模板扩展,因为它确定如果我的U适用,则需要我们检查我的!=是否适用。评论中链接的版本!=会产生明显的错误。)

但我的问题是:有没有办法可以说服我的SFINAE覆盖在决定它是否应该失败时忽略“本身”,或以某种方式摆脱自我指涉问题?或者将我的#if 1的优先级降低到足以使任何明确的operator!=获胜,即使它不是一个好的匹配?

不检查“!=不存在”的那个工作得相当好,但不足以让我像将它注入全局命名空间一样不礼貌。

目标是任何编译而没有我的“魔法”!=的代码在引入我的“魔法”!=后完全相同的事情。如果且且仅当!=无效 !=形成良好,我的“魔法”bool r = !(a==b)才会启动。


脚注 1 :如果您创建!=,SFINAE会认为每对类型之间都有一个有效的template<typename U, typename T> bool operator!=(U&& u, T&& t)。然后,当您尝试实际调用!=时,它将被实例化,并且无法编译。最重要的是,你踩踏!=函数,因为你更适合bool operator!=( const foo&, const foo& )foo() != foo()。我认为做这两种不礼貌。

1 个答案:

答案 0 :(得分:12)

您的方法的问题似乎是operator !=的后备全局定义过于吸引人了,您需要通过SFINAE检查来排除它。但是,SFINAE检查取决于函数本身是否有资格进行重载解析,从而导致在类型推导期间(尝试)无限递归。

在我看来,基于SFINAE的任何类似尝试都会在同一墙上崩溃,所以我认为最明智的方法是让你的operator !=首先对重载解析不那么有吸引力,并让其他合理的书面(这将在一瞬间清楚)operator !=的重载优先。

鉴于您提供的类型特征can_equal

#include <type_traits>
#include <functional>

template<typename T, typename U, typename=void>
struct can_equal : std::false_type {};

template<typename T, typename U>
struct can_equal<
   T,
   U,
   typename std::enable_if<
      std::is_convertible<
         decltype( std::declval<T>() == std::declval<U>() ),
         bool
      >::value
   >::type
>: std::true_type {};

我会以这种方式定义回退operator !=

template<typename T, typename U>
bool is_not_equal(T&& t, U&& u)
{
    return !(std::forward<T>(t) == std::forward<U>(u));
}

template<
    typename T,
    typename... Ts,
    typename std::enable_if<can_equal<T, Ts...>::value>::type* = nullptr
    >
bool operator != (T const& t, Ts const&... args)
{
    return is_not_equal(t, args...);
}

据我所知,operator !=的任何重载都将定义两个函数参数(因此没有参数包)将更适合重载解析。因此,仅当没有更好的过载时,才会选择上述operator !=的后备版本。此外,仅当can_equal<>类型特征将返回true时才会被选中。

我已根据您准备的SSCCE对此进行了测试,其中定义了四个struct以及operator ==operator !=的一些重载:

struct test { };

bool operator==(const test&, const test&) { std::cout << "(==)"; return true; }
bool operator!=(const test&, const test&) { std::cout << "(!==)"; return true; }

struct test2 { };

struct test3 { };
bool operator == (const test3&, const test3&) 
{ std::cout << "(==)"; return true; }

struct test4 { };

template<typename T, 
         EnableIf< std::is_convertible< T, test4 const& >::value >... >
bool operator == ( T&&, T&& ) { std::cout << "(==)"; return true; }

template<typename T, 
         EnableIf< std::is_convertible< T, test4 const& >::value >... >
bool operator != ( T&&, T&& ) { std::cout << "(!=)"; return true; }

要验证是否生成了所需的输出并镜像了您在原始版本的后备operator !=中所做的操作,我向is_not_equal()添加了打印输出:

template<typename T, typename U>
bool is_not_equal(T&& t, U&& u)
{
    std::cout << "!"; // <== FOR TESTING PURPOSES
    return !(std::forward<T>(t) == std::forward<U>(u));
}

以下是您示例中的三个测试:

std::cout << (a != b) << "\n"; // #1
std::cout << (test3() != test3()) << "\n"; // #2
std::cout << (test4() != test4()) << "\n"; // #3

关于第一次测试,operator !=是针对test类型定义的,因此应打印行#1

(!==)1

关于第二次测试,operator != test3定义,test3无法转换为test4,因此我们的全球{ {1}}应该发挥作用并否定operator !=超载的结果需要两个operator ==。因此,行const test3&应打印:

#2

最后,第三个测试涉及两个类型为!(==)0 // operator == returns true, and is_not_equal() negates it 的右值对象,其中定义了test4(因为参数可转换为operator !=)。因此,行test4 const&应打印:

#3

这是一个live example,显示产生的输出是预期的输出。