奇怪的重复模式和Sfinae
Curiously Recurring Pattern and Sfinae
我已经打算通过SFINAE和Curioly Recurring Template Pattern习语实现总排序有一段时间了。总体思路如下:
- 定义用于检查关系运算符(
<
、>
等)的模板 - 定义定义总排序运算符的基类
- 根据运算符检测定义基类
- 从基类继承
为了简单起见,我在本例中忽略了==
和!=
运算符。
关系运算符检测
我首先定义类来静态检查类是否定义了特定的功能。例如,在这里我检测到小于运算符或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_less
、has_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>
已经用不完整的A
和value == 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 { ...
这一点非常重要,可以防止以后编译使用这些类的代码时出现许多不明显的问题
- 具有奇怪重复模板模式的派生类中的成员变量已损坏
- 为什么在保护模式下继承升级不起作用
- 为什么使用SFINAE而不是函数重载
- 如何在全屏模式下(在OpenGL中)使背景透明
- 如何使用模板函数的函数签名进行SFINAE
- 数据成员SFINAE的C++17测试:gcc vs clang
- 使用在用于SFINAE的void_t中具有参数的方法
- 为什么使用__LINE_的代码在发布模式下在MSVC下编译,而不是在调试模式下
- 派生类是否可以在抽象工厂设计模式中具有数据成员
- 此模式的C++RegEx
- avrogencpp能为模式中的每种类型生成单独的头文件吗
- 使用可变模板的Broadcaster/Listener模式
- c++方法参数只能在linux的发布模式下自行更改
- 编译器如何在使用SFINAE的函数和标准函数之间确定两者是否可行
- 资源管理设计模式
- 提供与TMP和SFINAE的通用接口
- 使用 mod_gsoap 部署服务时,如何在 Gsoap 中更改 soap 上下文的模式?
- 检测与SFINAE的协同依赖功能的模式
- 奇怪的重复模式和Sfinae
- 模式匹配/SFINAE关于特定类的容器参数化