奇怪的重复模式和Sfinae

Curiously Recurring Pattern and Sfinae

本文关键字:模式 Sfinae      更新时间:2023-10-16

我已经打算通过SFINAE和Curioly Recurring Template Pattern习语实现总排序有一段时间了。总体思路如下:

  1. 定义用于检查关系运算符(<>等)的模板
  2. 定义定义总排序运算符的基类
  3. 根据运算符检测定义基类
  4. 从基类继承

为了简单起见,我在本例中忽略了==!=运算符。

关系运算符检测

我首先定义类来静态检查类是否定义了特定的功能。例如,在这里我检测到小于运算符或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;

总订购

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

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

基类

然后,我定义了一个基类,它创建了一个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

继承

所有这些步骤,单独地,都是有效的。然而,当我实际使用奇怪的重复模式从基类继承时,生成的类只实现了其中一个运算符,并且检测算法失败了。

示例

我已经将这个问题归结为一个由核心部分组成的最小示例:运算符检测(has_lesshas_greater)、总排序实现(total)、简化基类(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, "");

不幸的是,基类在继承过程中没有检测到运算符,SFINAE也没有检测到结果类中的小于或大于运算符。

问题和跟进

我想知道为什么会失败,因为我已经用奇怪的重复模式做了很长时间的成员函数检测和成员类型检测,没有问题。假设我的代码没有直接的问题,那么有没有办法以这种方式实现完全排序?

编辑

我能够使用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; }
};

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

这方面的主要问题是,当has_less<A>被实例化时(在将total<A>实例化为A的基类期间),A是一个不完整的类型——此时,编译器还不知道A有一个operator <

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

在所有这些决定之后,编译器看到A::operator <的定义,并将其添加到A中。在此之后,A完成。

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

要显示这种情况,请复制has_less,将其命名为has_less2,并将静态断言更改为static_assert(has_less2<A>::value, "");。因为has_less2<A>是在A得到其小于运算符之后实例化的,所以此断言成功。

使代码成功的一种方法是转发声明A并声明一个全局operator <,用于比较两个A对象,这样编译器在计算出A的基类之前就知道了这个运算符。类似这样的东西:

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设置自动发生,而在派生类中不需要任何特殊的调节,那会更好。但这可能仍然会给你一些见解,帮助你找到合适的解决方案。我也会进一步考虑这个问题,如果我有什么想法,我会更新这个答案。

还有一件事:比较成员函数应该是const限定的。像

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

这一点非常重要,可以防止以后编译使用这些类的代码时出现许多不明显的问题