在C++20之前,想按照结构体的某个成员对容器排序,通常需要专门写一个比较函数或者lambda表达式,把两个对象的字段取出来再比较。C++20引入的std::ranges::sort带来了投影这一特性,允许在排序时直接指定一个函数对象,算法会先把每个元素投影成参与比较的值,再交给比较器处理。这种写法不仅代码更短,而且意图更清晰,是现代C++算法库里非常实用的功能。本文将从原理到实践,完整讲解投影函数的用法。

投影函数的基本原理与语法
所谓投影,指的是算法在对元素做比较之前,先对每个元素应用一个一元函数,把元素转换成真正参与比较的形式。以ranges::sort为例,它的完整签名中最后一个参数就是投影对象。排序过程中,算法对每一对元素分别调用投影,再把投影结果传给比较器。等价于逻辑上的comp(proj(a), proj(b))。
投影最常见的形式是传入一个成员指针。比如有一个Employee结构体,想按年龄排序,投影直接写&Employee::age即可,不需要再手写lambda。来看一个完整的例子:
#include <algorithm>
#include <vector>
#include <iostream>
#include <string>
struct Employee {
std::string name;
int age;
double salary;
};
int main() {
std::vector<Employee> staff = {
{"张三", 32, 8500.0},
{"李四", 28, 9200.0},
{"王五", 35, 7800.0}
};
// 按年龄升序排序,&Employee::age 就是投影
std::ranges::sort(staff, std::less{}, &Employee::age);
for (const auto& e : staff) {
std::cout << e.name << " " << e.age << "\n";
}
}
这段代码输出会按照28、32、35的顺序打印三名员工。注意比较器参数可以省略,省略时默认使用std::less{},也就是升序排序,写成std::ranges::sort(staff, {}, &Employee::age)也可以。投影与比较器是两个独立的维度:投影负责取值,比较器负责定义大小关系,两者组合能覆盖绝大多数排序需求。
降序排序与自定义比较器配合投影
投影本身不决定升序还是降序,方向由比较器控制。想按工资从高到低排序,把比较器换成std::greater{}即可。这种职责分离的设计让代码复用性很高,同一个投影可以搭配不同比较器。
// 按工资降序排序
std::ranges::sort(staff, std::greater{}, &Employee::salary);
// 等价的传统写法,对比一下复杂度
std::sort(staff.begin(), staff.end(),
[](const Employee& a, const Employee& b) {
return a.salary > b.salary;
});
对比可以看出,传统lambda写法需要写完整的参数列表、取成员、写比较逻辑,而投影写法只声明意图,省去了样板代码。此外投影还带来一个额外的好处:如果比较器写错了,比如对不相干的字段做了比较,投影版本根本编译不过,类型检查更严格。
当默认的less和greater不满足需求时,可以传入自定义lambda作为比较器,投影照常工作。例如按字符串长度排序:
// 按姓名长度从短到长排序
std::ranges::sort(staff,
[](const std::string& a, const std::string& b) {
return a.size() < b.size();
},
&Employee::name);
这里比较器接收的类型是投影结果的类型,也就是std::string,而不是Employee本身。理解这一点很关键,很多初学者在写比较器时仍然把参数写成Employee类型,导致编译报错后一头雾水。记住执行顺序永远是先投影、后比较。
多字段组合排序的进阶技巧
实际业务中经常需要多级排序,比如先按部门排,部门相同的再按入职时间排。投影只接受一个函数对象,但这个对象可以是lambda,内部返回一个tuple或pair。std::tuple的比较规则是逐元素字典序比较,天然适合实现多级排序。
#include <tuple>
struct Record {
std::string dept; // 部门
int hire_year; // 入职年份
int age;
};
std::vector<Record> data = { /* 假设已有数据 */ };
// 先按部门升序,部门相同再按入职年份降序,最后按年龄升序
std::ranges::sort(data, {}, [](const Record& r) {
return std::tie(r.dept, std::greater{}(0, r.hire_year) ? 0 : r.hire_year, r.age);
});
上面的写法对年份降序处理得比较绕,更推荐的做法是取相反数。对于数值类型,降序字段直接返回负值即可:
// hire_year 取相反数,实现该字段降序,其余字段保持升序
std::ranges::sort(data, {}, [](const Record& r) {
return std::make_tuple(r.dept, -r.hire_year, r.age);
});
需要提醒的是,std::tie返回的是引用元组,能避免拷贝,适合字段本身可以复制或本身就是左值的场景。如果字段需要经过计算得到(比如取相反数),就得用make_tuple生成值元组。另外浮点字段取负实现降序没有问题,但对于INT_MIN这类边界值取负会溢出,遇到这种情况建议改用自定义比较器方案。
嵌套结构与成员函数的投影写法
投影不局限于直接成员,也可以是任意可调用对象。当要排序的字段藏在嵌套结构里时,用lambda返回嵌套字段即可。如果类型已经重载了operator()或者提供了返回排序键的成员函数,同样能直接作为投影使用。
struct Address {
std::string city;
int postcode;
};
struct Person {
std::string name;
Address addr;
int score() const { /* 某种计算逻辑 */ return 42; }
};
std::vector<Person> people = { /* 数据 */ };
// 按嵌套的城市字段排序
std::ranges::sort(people, {}, &Person::addr, );
// 上面写法有误,正确做法是用lambda返回嵌套成员:
std::ranges::sort(people, {}, [](const Person& p) {
return p.addr.city;
});
// 调用成员函数作为投影,注意要写成成员函数指针
std::ranges::sort(people, std::greater{}, &Person::score);
成员函数指针作为投影时,算法会对每个元素调用该函数,把返回值当作比较依据,这个特性在类型带有计算属性时特别方便。不过要注意成员函数必须是const的,否则对const引用调用会编译失败。
最后补充几点使用建议。第一,投影在每个比较中都会被调用,如果投影操作开销很大(比如深拷贝字符串),排序整体性能会受影响,此时可以预先把排序键提取出来配合索引排序。第二,投影返回的临时对象生命周期由编译器保证安全,不必担心悬空引用。第三,头文件使用<algorithm>即可,部分旧编译器需要额外确认对C++20 ranges的支持情况,GCC 10以上、MSVC 19.29以上都具备完整实现。掌握投影之后,你会发现几乎所有的ranges算法都支持这个参数,find、max_element、unique等算法同样可以用投影来简化代码,值得在日常开发中推广使用。
std::ranges::sort投影函数C++结构体排序修改时间:2026-09-15 20:30:40