C++23 标准把 std::ranges::contains 正式加入了范围算法库。它的作用非常直接:给定一个范围和一个值,返回一个布尔值表示该值是否出现在范围中。与 std::ranges::find 返回迭代器相比,contains 把“是否存在”这个最常见需求固化成了更短的表达方式,尤其适合只关心有没有、不关心位置的场景。

这个接口在关联容器上配合视图使用时尤其顺手。过去如果想知道一个 std::map 里是否存在某个键,可以调用成员函数 contains;但如果想用统一的算法风格,也可以把键提取成一个惰性范围再调用 std::ranges::contains。下面分别从基础用法、键值查找、性能差异和编译条件几个角度展开。
一、std::ranges::contains 的基本用法与接口说明
最常用的 std::ranges::contains 重载形式可以近似理解为:template<ranges::input_range R, class T> constexpr bool contains(R&& r, const T& value);。它接受一个范围 r 和一个待查找的值 value,返回值为 bool。如果范围中至少有一个元素等于 value,结果就是 true,否则为 false。这个算法还提供了带投影和谓词的版本,允许在结构体里按某个字段查找。
对普通类数组容器来说,std::ranges::contains 等价于 std::ranges::find(r, value) != std::ranges::end(r),但它把两次调用合并成一次,既减少了代码量,也避免了手写 end 时可能出现的临时范围问题。下面的例子演示了对 std::vector<int> 做包含判断的基本写法。
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v{1, 2, 3, 4, 5};
bool has3 = std::ranges::contains(v, 3);
bool has10 = std::ranges::contains(v, 10);
std::cout << std::boolalpha;
std::cout << "contains 3: " << has3 << '\n';
std::cout << "contains 10: " << has10 << '\n';
}
这段代码只需要包含 <algorithm> 和 <vector> 两个头文件。输出结果会显示 true 和 false。可以看到,调用站点不再出现 begin、end、== 这类辅助词汇,意图非常清晰。
需要注意的是,std::ranges::contains 与 std::ranges::find 一样,对普通范围执行线性扫描。也就是说,如果元素数量为 N,最坏情况下会检查所有 N 个元素。它并不会因为名字叫 contains 就自动获得二分查找或哈希查找的复杂度。因此它更适合元素数量不大、或者本来就必须遍历所有元素的场景。
二、在关联容器中查找键和值
对于 std::map、std::unordered_map 这类关联容器,C++20 已经增加了成员函数 contains,可以直接判断某个键是否存在,例如 m.contains(key)。而 C++23 的范围算法则提供了一种更统一的风格:通过 std::views::keys 提取键视图,再交给 std::ranges::contains 判断。这样写的好处是,不关心容器内部是红黑树还是哈希表,只要它支持键值对迭代,视图就能统一工作。
用 std::views::keys 时,管道表达式 m | std::views::keys 会产生一个惰性视图,其中每个元素都是原容器的键。它并不复制任何数据,所以创建视图的开销很小。类似地,std::views::values 可以提取所有值。下面这段代码对一个 std::unordered_map<std::string, int> 分别做了键判断和值判断。
#include <algorithm>
#include <ranges>
#include <unordered_map>
#include <iostream>
#include <string>
int main() {
std::unordered_map<std::string, int> scores{
{"alice", 90},
{"bob", 85},
{"carol", 92}
};
// 推荐:使用成员函数 contains,平均 O(1)
bool hasAlice = scores.contains("alice");
// 用 ranges::contains 配合 keys 视图,语法简洁但为线性扫描
auto keys_view = scores | std::views::keys;
bool hasBob = std::ranges::contains(keys_view, std::string{"bob"});
// 检查值集合
auto values_view = scores | std::views::values;
bool hasScore85 = std::ranges::contains(values_view, 85);
std::cout << std::boolalpha
<< hasAlice << ' '
<< hasBob << ' '
<< hasScore85 << '\n';
}
这里有一个很容易被忽视的性能差异。成员函数 unordered_map::contains 的平均时间复杂度是 O(1),最坏情况与哈希冲突有关;而 std::ranges::contains 对 keys_view 的遍历是线性的,时间复杂度和元素数量成正比。对于只有三个元素的示例来说没有差别,但如果 map 有几十万甚至上百万条记录,线性和 O(1) 的差距就会非常明显。
同理,std::map 的成员 contains 是 O(log n),而范围算法配 keys_view 仍然是 O(n)。因此在实际开发中,如果容器本身提供了 contains,并且查找键是主要目标,应该优先调用成员函数。只有当你需要统一处理多种容器、或者容器是自建类型且不支持成员 contains 时,再考虑用范围算法加视图的方案。
三、与传统 find 方案及其他 contains 成员对比
C++20 里已经有 std::ranges::find,它返回迭代器,判断是否存在还需要再和 std::ranges::end 做一次比较。C++23 的 std::ranges::contains 把这一判断直接封装成 bool,并且同样支持投影参数。例如有一个结构体 Item,里面包含 id 和 name,想知道容器里是否存在 id 为 10 的对象,可以写成 std::ranges::contains(items, 10, &Item::id),不再需要手写 lambda。
下面的代码把成员投影版本与传统 find 写法放在一起对比。注意投影参数 &Item::id 是一个成员指针,它告诉算法在比较前先取出每个元素的 id 字段。这样既避免了 == 运算符的重载,也让代码保持简洁。
#include <algorithm>
#include <vector>
#include <ranges>
#include <iostream>
#include <string>
struct Item {
int id;
std::string name;
};
int main() {
std::vector<Item> items{{1, "pen"}, {2, "book"}, {10, "lamp"}};
// 使用投影只比较 id 字段
bool hasId10 = std::ranges::contains(items, 10, &Item::id);
// 传统 find 写法
auto it = std::ranges::find(items, 10, &Item::id);
bool hasId10_old = it != std::ranges::end(items);
std::cout << std::boolalpha
<< hasId10 << ' '
<< hasId10_old << '\n';
}
除了 std::ranges::contains,标准库中还有一些名字相似的成员函数。下表列出了不同查找方式在返回类型、典型复杂度和推荐场景上的差异。
| 查找方式 | 返回类型 | 典型复杂度 | 推荐场景 |
|---|---|---|---|
| std::ranges::contains | bool | O(n) | 普通范围、只需存在性判断 |
| std::ranges::find | 迭代器 | O(n) | 需要位置或后续操作 |
| std::map::contains | bool | O(log n) | 有序关联容器键查找 |
| std::unordered_map::contains | bool | 平均 O(1) | 无序关联容器键查找 |
从表中可以看出,std::ranges::contains 的优势在于通用性和简洁性,而不是绝对性能。如果数据规模很大,并且容器本身提供了更快的查找接口,应当优先使用成员函数。如果数据已经存在于 std::vector、std::list 或 std::array 中,又没有排序,那么 std::ranges::contains 就是最自然的选择。
另一个常见误区是,有人会把 std::ranges::contains 当作二分查找的替代品。二分查找要求范围有序,并且使用 std::ranges::binary_search 才是 O(log n)。如果范围已经排序,要频繁判断存在性,用 binary_search 比线性 contains 快得多;但如果每次查找都排序,那成本反而更高。因此选型时要同时考虑数据规模、查找频率和容器是否有序。
四、编译条件与常见错误处理
std::ranges::contains 是 C++23 的新特性,需要在较新的编译器中开启对应标准。GCC 13 以上、Clang 17 以上以及新版本的 MSVC 通常已经支持。命令行编译时加上 -std=c++23,CMake 工程可以设置 CMAKE_CXX_STANDARD 为 23;如果使用 MSVC,也可以使用 /std:c++latest。
可以通过特性测试宏来判断当前标准库是否提供了这个算法。宏名是 __cpp_lib_ranges_contains,只要它被定义并且值至少为 202207L,就表示 std::ranges::contains 可用。下面是一个条件编译示例,在不支持的新标准下回退到自定义实现。
#include <algorithm>
#include <vector>
#if defined(__cpp_lib_ranges_contains)
// C++23 标准库直接支持
bool check_contains(const std::vector<int>& v, int target) {
return std::ranges::contains(v, target);
}
#else
// 兼容旧标准:手写一个简单版本
template<typename Range, typename T>
bool my_contains(const Range& range, const T& value) {
return std::find(std::begin(range), std::end(range), value) != std::end(range);
}
bool check_contains(const std::vector<int>& v, int target) {
return my_contains(v, target);
}
#endif
int main() {
std::vector<int> v{1, 2, 3, 4};
bool ok = check_contains(v, 3);
(void)ok;
}
初学者最容易遇到的编译错误是在 std::map 上直接调用 std::ranges::contains(m, key)。因为 std::map 的元素类型是 std::pair<const K, V>,而 key 通常是 K 类型,两者无法直接比较,编译器会报出一长串模板错误。正确做法是使用 m | std::views::keys 先提取键视图,或者直接调用 m.contains(key)。
另一个容易出错的地方是漏掉头文件。虽然 std::ranges::contains 在 <algorithm> 中,但 std::views::keys 和 std::views::values 需要包含 <ranges>。如果只包含 <algorithm> 却使用视图,编译器会提示 views 不存在。把这两个头文件都包含进去,基本就能避免大多数与 contains 相关的编译问题。
总体来说,std::ranges::contains 是 C++23 一个非常实用的小改进。它没有引入新的性能魔法,却让存在性判断的代码更短、更可读。对于还在使用旧标准的项目,也可以先写一个简单的模板函数来模拟同样的体验。掌握它的能力边界,在正确场景下使用,可以显著减少冗余代码。
std::ranges::containsC++23标准库键值查找修改时间:2026-09-29 23:36:07