共计 1760 个字符,预计需要花费 5 分钟才能阅读完成。
最近在代码 review 时发现一个性能问题:某接口直接传递了包含 10 万元素的 set,导致每次调用产生明显延迟。通过这个案例,我们来看看如何正确在函数间传递 set 容器。

1. 为什么不能直接传 set?
直接值传递 set 会导致整个容器被复制。对于包含大量元素的 set 来说,这个拷贝开销非常昂贵。例如:
void processSet(std::set<int> data) {/*...*/} // 错误示范
当调用 processSet(hugeSet) 时,所有元素都会被逐个拷贝。通过简单测试可以看到(测试环境:i7-10750H, 16GB DDR4):
std::set<int> bigSet;
// 插入 100,000 个随机数
for(int i=0; i<100000; ++i)
bigSet.insert(rand());
auto start = std::chrono::high_resolution_clock::now();
processSet(bigSet); // 值传递
auto end = std::chrono::high_resolution_clock::now();
std::cout << "耗时:"
<< std::chrono::duration_cast<std::chrono::milliseconds>(end-start).count()
<< "ms";
测试结果显示,仅容器拷贝就花费了约 15ms,这在性能敏感的场景是不可接受的。
2. 正确的传参方式
方法一:const 引用传递
void processSet(const std::set<int>& data) {/*...*/}
- 优点:完全避免拷贝,原集合不会被修改
- 缺点:无法获取数据所有权
方法二:移动语义
void processSet(std::set<int>&& data) {/*...*/}
// 调用时
processSet(std::move(mySet)); // 转移所有权
- 优点:零拷贝,获取数据所有权
- 注意:调用后原 set 变为空
方法三:迭代器区间
template<typename Iterator>
void processRange(Iterator begin, Iterator end) {/*...*/}
// 调用时
processRange(mySet.begin(), mySet.end());
- 优点:泛用性强,适用于各种容器
- 缺点:无法直接使用 set 特有的方法
3. 进阶技巧
自定义比较器实现
struct Person {
std::string name;
int age;
};
struct PersonCompare {bool operator()(const Person& a, const Person& b) const {
// 必须满足严格弱序
return std::tie(a.name, a.age) < std::tie(b.name, b.age);
}
};
std::set<Person, PersonCompare> personSet;
线程安全封装
class ThreadSafeSet {
std::set<int> data_;
std::mutex mtx_;
public:
void insert(int value) {std::lock_guard<std::mutex> lock(mtx_);
data_.insert(value);
}
// 其他方法...
};
4. 性能对比测试
| 传参方式 | 10 万元素耗时 | 100 万元素耗时 |
|---|---|---|
| 直接值传递 | 15ms | 210ms |
| const 引用 | <1ms | <1ms |
| 移动语义 | <1ms | <1ms |
| 迭代器区间 | <1ms | <1ms |
5. 必须避开的坑
-
不要修改 set 元素:set 元素是 const 的,修改会导致未定义行为
for(auto& x : mySet) {x = newValue; // 编译错误!} -
比较器必须满足严格弱序:否则会导致容器行为异常
-
迭代器失效问题:在遍历时不要插入 / 删除元素
6. 思考题
- 当需要频繁查询时:
- set 保证 O(logN)查询
- unordered_set 平均 O(1)但最差 O(N)
-
根据数据特征选择
-
混合容器设计:
- 考虑结合 vector 和 set 的特性
- 或用 boost::multi_index 实现多索引
通过本文,你应该已经掌握了高效使用 set 容器的关键技巧。记住:避免不必要的拷贝、正确使用移动语义、注意线程安全,这些原则同样适用于其他 STL 容器。
正文完
