如何判断一个类型是否可比较

对于一个用户定义类型,如果定义了operator==或者operator<等比较函数,那么这个类型就是可比较的。在实现一些工具库的时候,可能需要知道某个类型是否可比较,例如,想要知道是否可以使用==操作符来比较两个类对象是否相等,那么可以使用下面的模板类来判断:

1
2
3
4
5
6
7
8
9
10
11
12
template<typename T>
class IsEqualityComparable {
private:
template<typename K>
static constexpr auto Test(K*) -> decltype(std::declval<K>() == std::declval<K>());

template<typename K>
static constexpr int Test(...);

public:
static constexpr bool Value = std::is_same_v<bool, decltype(Test<T>(nullptr))>;
};

使用方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//内置类型和标准库类型
std::cout << IsEqualityComparable<int>::Value; // true
std::cout << IsEqualityComparable<std::string>::Value; // true

//有比较操作符的自定义类型
class ComaprableObject {
//...
};
bool operator==(const ComaprableObject& o1, const ComaprableObject& o2) {
//...
}
std::cout << IsEqualityComparable<ComaprableObject>::Value // true

//没有比较操作符的自定义类型
class UncomparableObject {
//...
};
std::cout << IsEqualityComparable<ComaprableObject>::Value // false

这个模板类型基于C++的SFINAE特性,关于这个特性的解释,可以参考之前的文章《如何判断一个容器是否关联容器》,在这篇文章里,也基于SFINAE特性实现了一个判断某类型是否关联容器的模板类。

IsEqualityComparable使用了std::is_same_v来推导结果,它检查Test<T>(nullptr)这个函数调用表达式的返回值类型是否bool,如果是,则T可以用==进行比较,如果不是则不可以比较。

为了让Test<T>(nullptr)这个表达式对不同类型有不同的返回值,这里定义了两个重载的Test模板方法,其中第一个的声明如下所示:

1
2
template<typename K>
static constexpr auto Test(K*) -> decltype(std::declval<K>() == std::declval<K>());

这个方法的返回值类型通过decltype来推导,而推导的来源正是模板类型K==表达式,如果K可以比较,那么这个方法是有效的,返回值类型是bool;如果K不可以比较,那么根据SFINAE,这个方法会被删除,就像从未存在过一样。注意std::declval的使用,这个函数可以在不调用构造函数的前提下生成一个K的引用,以便使用者直接访问K的接口。这意味着使用者不需要知道如何构造K的对象,因为==只能作用于对象上,在比较之前必须先有对象,为了创建对象,使用者要调用K的构造方法。由于每个类型的构造方法都不一样,这里就没办法做到广泛适用。而std::declval绕过了构造方法,避免了这个问题。

第二个重载的Test模板方法如下所示:

1
2
template<typename K>
static constexpr int Test(...);

这个方法是“没有选择时的最后选择“。如果第一个模板方法被删除了,那么Test<T>(nullptr)这个表达式就会匹配到这个模板方法,表达式的返回值类型是int;如果第一个模板方法有效,那么表达式会优先匹配第一个方法,因为参数为...的重载方法优先级是最低的,此时表达式的返回值类型是bool