更改集的默认排序

setmultiset 有默认的比较方法,但在某些情况下你可能需要重载它们。

让我们假设我们将字符串值存储在一个集合中,但我们知道这些字符串只包含数值。默认情况下,排序将是字典字符串比较,因此顺序与数字排序不匹配。如果要应用与 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::pairfirst 的值进行比较),它经常被使用。

创建比较运算符时,这应该是一个稳定的排序。如果在插入后比较运算符的结果发生更改,则你将具有未定义的行为。作为一种好的做法,比较运算符应该只使用常量数据(const 成员,const 函数……)。

如上例所示,你经常会遇到没有成员的类作为比较运算符。这导致默认构造函数和复制构造函数。默认构造函数允许你在构造时省略实例,并且需要复制构造函数,因为该集合采用了 compare 运算符的副本。

Lambda 排序

Lambda 是编写函数对象的较短方法。这允许在较少的行上编写比较运算符,使整个代码更具可读性。

使用 lambdas 的缺点是每个 lambda 在编译时都会获得一个特定的类型,因此 decltype(lambda) 对于同一个编译单元(cpp 文件)的每次编译都会与多个编译单元(当通过头文件包含时)不同。因此,建议在头文件中使用函数对象作为比较运算符。

当在函数的局部范围内使用 std::set 时经常遇到这种结构,而当用作函数参数或类成员时,首选函数对象。

其他排序选项

由于 std::set 的比较运算符是模板参数,因此所有可调用对象都可以用作比较运算符,上面的示例只是特定情况。这些可调用对象的唯一限制是:

  • 它们必须是可复制的
  • 它们必须可以使用键类型的 2 个参数进行调用。 (允许隐式转换,但不推荐,因为它会影响性能)