奇怪的重复模式和 Sfinae

Curiously Recurring Pattern and Sfinae

一段时间以来,我一直想通过 SFINAE 和 Curiously Recurring Template Pattern 习语来实现总排序。大致思路如下:

  1. 定义用于检查关系运算符(<> 等)的模板
  2. 定义定义总排序运算符的基 classes。
  3. 从操作符检测中定义一个碱基 class。
  4. 继承自基础 class。

为简单起见,我忽略了此示例中的 ==!= 运算符。

关系运算符检测

我首先定义了classes来静态检查class是否定义了一个特定的特性。例如,在这里我检测到小于运算符或 operator<.

的存在
template <typename T>
class has_less
{
protected:
    template <typename C> static char &test(decltype(std::declval<C>() < std::declval<C>()));
    template <typename C> static long &test(...);

public:
    enum {
        value = sizeof(test<T>(0)) == sizeof(char)
    };
};

template <typename T>
constexpr bool has_less_v = has_less<T>::value;

总订购量

然后我定义 classes 来实现给定运算符的总排序,例如,要定义小于运算符的总排序,我将使用以下内容:

template <typename T>
struct less_than_total
{
    bool operator>(const T &t) { return t < static_cast<T&>(*this); }
    bool operator>=(const T &t) { return !(static_cast<T&>(*this) < t); }
    bool operator<=(const T &t) { return !(t < static_cast<T&>(*this)); }
}; 

基础Class

然后我定义了一个基础 class,它创建了一个 typedef 以通过检测已实现的运算符来实现剩余的运算符。

template <bool B, typename T, typename F>
using conditional_t = typename std::conditional<B, T, F>::type;

template <typename T>
using total_ordering = conditional_t<           // has_less
    has_less_v<T>,
    less_than_total<T>,
    conditional_t<                              // has_less_equal
        has_less_equal_v<T>,
        less_equal_total<T>,
        conditional_t<                          // has_greater
            has_greater_v<T>,
            greater_total<T>,
            conditional_t<                      // has_greater_equal
                has_greater_equal_v<T>,
                greater_equal_total<T>,
                symmetric<T>                    // symmetry
            >                                   // has_greater_equal
        >                                       // has_greater
    >                                           // has_less_equal
>;                                              // has_less

继承

所有这些步骤,单独地,工作。但是,当我使用奇怪的重复模式实际从基础 class 继承时,生成的 class 仅实现这些运算符之一,检测算法失败。

例子

我已将问题归结为一个由核心部分组成的最小示例:运算符检测(has_lesshas_greater)、总排序实现(total)、a简化的基础 class (total),以及使用这些关系运算符的简单结构 (A).

#include <type_traits>


// DETECTION

template <typename T>
class has_less
{
protected:
    template <typename C> static char &test(decltype(std::declval<C>() < std::declval<C>()));
    template <typename C> static long &test(...);

public:
    enum {
        value = sizeof(test<T>(0)) == sizeof(char)
    };
};


template <typename T>
class has_greater
{
protected:
    template <typename C> static char &test(decltype(std::declval<C>() > std::declval<C>()));
    template <typename C> static long &test(...);

public:
    enum {
        value = sizeof(test<T>(0)) == sizeof(char)
    };
};


// TOTAL ORDERING


template <typename T>
struct less_than_total
{
    bool operator>(const T &t) { return t < static_cast<T&>(*this); }
    bool operator>=(const T &t) { return !(static_cast<T&>(*this) < t); }
    bool operator<=(const T &t) { return !(t < static_cast<T&>(*this)); }
};


template <typename T>
struct symmetry
{};


template <bool B, typename T, typename F>
using conditional_t = typename std::conditional<B, T, F>::type;


template <typename T>
struct total: conditional_t<
        has_less<T>::value,
        less_than_total<T>,
        symmetry<T>
    >
{};


// TEST

struct A: total<A>
{
    bool operator<(const A &r)
    {
        return true;
    }
};



int main(void)
{
    static_assert(has_less<A>::value, "");
    static_assert(has_greater<A>::value, "");
    return 0;
}

理想情况下,这个例子可以编译,但是,我得到:

$ clang++ a.cpp -o a -std=c++14
a.cpp:79:5: error: static_assert failed ""
    static_assert(has_less<A>::value, "");
    ^             ~~~~~~~~~~~~~~~~~~
a.cpp:80:5: error: static_assert failed ""
    static_assert(has_greater<A>::value, "");

不幸的是,基础 class 没有在继承过程中检测运算符,并且 SFINAE 没有检测结果 class.

中的小于或大于运算符

问题与跟进

我想知道为什么会失败,因为我已经用奇怪的重复模式进行了很长时间的成员函数检测和成员类型检测,没有问题。假设我的代码没有直接问题,是否有任何解决方法可以以这种方式实现总排序?

编辑

我可以使用 std::enable_if 实现我想要的一部分。在这种情况下,唯一简单的答案是根据 operator< 实现所有内容,然后从该运算符实现总排序。

template <typename T>
struct total
{
    template <typename U = T>
    typename std::enable_if<has_less<U>::value, bool>::type
    bool operator>(const T &l, const T &r) { return r < t; }
};

If 仍然想知道为什么我通过 SFINAE 进行的运算符检测在继承过程中失败,但对继承方法成功。

这样做的主要问题是 A 在实例化 has_less<A> 时是一个不完整的类型(在实例化 total<A> 作为 A 的基础 class) -- 此时,编译器还不知道 A 有一个 operator <.

所以,has_less<A> 是用它的 value == 0 实例化的,symmetry<A> 是为 total<A> 的基础 class 选择的——所以 A 永远不会得到它的任何附加运算符。

在所有这些都确定之后,编译器会看到 A::operator < 的定义,并将其添加到 A。这之后,A就完成了。

所以我们知道 static_assert(has_greater<A>::value, ""); 失败的原因,但我们不应该期望 static_assert(has_less<A>::value, ""); 成功吗?毕竟,现在 A 有一个 less-than 运算符。问题是,has_less<A> 已经用不完整的 Avalue == 0 实例化了——即使 A 已经改变,也没有更新先前实例化的 [=62] 的机制=] 值。所以这个断言也失败了,即使它看起来应该成功。

为了证明是这种情况,请复制 has_less,将其命名为 has_less2,并将静态断言更改为 static_assert(has_less2<A>::value, "");。因为 has_less2<A>A 得到它的 less-than 运算符之后被实例化,所以这个断言成功。

使代码成功的一种方法是 forward-declare A 并声明一个全局 operator <,用于比较两个 A 对象,以便编译器知道在计算出 A 的基数 class 之前关于这个运算符。像这样:

struct A;
bool operator < (const A &lh, const A& rh);

struct A : total<A> {
    friend bool operator < (const A &lh, const A& rh) {
        return true;
    }
};

我知道这并不是您真正想要的,但是 — 如果 CRTP 设置自动发生,派生 class 中不需要任何特殊调整,那就更好了。但这可能仍然会给您一些洞察力,帮助您找到合适的解决方案。我也会再考虑一下,如果我有什么想法,我会更新这个答案。

还有一件事:比较成员函数应该是 const 合格的。喜欢

bool operator>(const T &t) const { ...

这非常重要,可以防止以后在编译使用这些 class 的代码时出现很多 non-obvious 问题。