更改集的默认排序
set
和 multiset
有默认的比较方法,但在某些情况下你可能需要重载它们。
让我们假设我们将字符串值存储在一个集合中,但我们知道这些字符串只包含数值。默认情况下,排序将是字典字符串比较,因此顺序与数字排序不匹配。如果要应用与 int
值相同的排序,则需要一个仿函数来重载 compare 方法:
#include <iostream>
#include <set>
#include <stdlib.h>
struct custom_compare final
{
bool operator() (const std::string& left, const std::string& right) const
{
int nLeft = atoi(left.c_str());
int nRight = atoi(right.c_str());
return nLeft < nRight;
}
};
int main ()
{
std::set<std::string> sut({"1", "2", "5", "23", "6", "290"});
std::cout << "### Default sort on std::set<std::string> :" << std::endl;
for (auto &&data: sut)
std::cout << data << std::endl;
std::set<std::string, custom_compare> sut_custom({"1", "2", "5", "23", "6", "290"},
custom_compare{}); //< Compare object optional as its default constructible.
std::cout << std::endl << "### Custom sort on set :" << std::endl;
for (auto &&data : sut_custom)
std::cout << data << std::endl;
auto compare_via_lambda = [](auto &&lhs, auto &&rhs){ return lhs > rhs; };
using set_via_lambda = std::set<std::string, decltype(compare_via_lambda)>;
set_via_lambda sut_reverse_via_lambda({"1", "2", "5", "23", "6", "290"},
compare_via_lambda);
std::cout << std::endl << "### Lambda sort on set :" << std::endl;
for (auto &&data : sut_reverse_via_lambda)
std::cout << data << std::endl;
return 0;
}
输出将是:
### Default sort on std::set<std::string> :
1
2
23
290
5
6
### Custom sort on set :
1
2
5
6
23
290
### Lambda sort on set :
6
5
290
23
2
1
在上面的示例中,可以找到 3 种不同的方法来向 std::set
添加比较操作,每种方法在其自己的上下文中都很有用。
默认排序
这将使用键的比较运算符(第一个模板参数)。通常,密钥已经为 std::less<T>
函数提供了一个很好的默认值。除非此功能是专用的,否则它使用对象的 operator<
。当其他代码也尝试使用某种排序时,这尤其有用,因为这样可以在整个代码库中保持一致性。
以这种方式编写代码将减少在密钥更改为 API 时更新代码的工作量,例如:包含 2 个成员的类,这些成员将更改为包含 3 个成员的类。通过更新类中的 operator<
,所有实例都将更新。
正如你所料,使用默认排序是合理的默认值。
自定义排序
当默认比较不符合时,通常使用带有比较运算符的对象添加自定义排序。在上面的例子中,这是因为字符串是指整数。在其他情况下,当你想根据它们所引用的对象比较(智能)指针或者因为你需要不同的约束来进行比较时(例如:将 std::pair
与 first
的值进行比较),它经常被使用。
创建比较运算符时,这应该是一个稳定的排序。如果在插入后比较运算符的结果发生更改,则你将具有未定义的行为。作为一种好的做法,比较运算符应该只使用常量数据(const 成员,const 函数……)。
如上例所示,你经常会遇到没有成员的类作为比较运算符。这导致默认构造函数和复制构造函数。默认构造函数允许你在构造时省略实例,并且需要复制构造函数,因为该集合采用了 compare 运算符的副本。
Lambda 排序
Lambda 是编写函数对象的较短方法。这允许在较少的行上编写比较运算符,使整个代码更具可读性。
使用 lambdas 的缺点是每个 lambda 在编译时都会获得一个特定的类型,因此 decltype(lambda)
对于同一个编译单元(cpp 文件)的每次编译都会与多个编译单元(当通过头文件包含时)不同。因此,建议在头文件中使用函数对象作为比较运算符。
当在函数的局部范围内使用 std::set
时经常遇到这种结构,而当用作函数参数或类成员时,首选函数对象。
其他排序选项
由于 std::set
的比较运算符是模板参数,因此所有可调用对象都可以用作比较运算符,上面的示例只是特定情况。这些可调用对象的唯一限制是:
- 它们必须是可复制的
- 它们必须可以使用键类型的 2 个参数进行调用。 (允许隐式转换,但不推荐,因为它会影响性能)