透明操作符函数对象

透明操作符函数对象
苏丙榅1. 何为透明
在 C++14 之前,标准库中的函数对象(如 std::less)是单态的,这意味着你必须在使用时指定具体的类型。C++14 引入了透明版本,即模板参数为 void 的版本(如 std::less<void>)。
所谓的透明,指的是该函数对象不关心参数的具体类型。它就像一个透明的代理,直接将参数传递给底层的运算符(如 operator<),而不强制进行类型转换。
- 不透明(C++11):
std::less<int>- 必须传入
int类型。如果你传一个short或者double,虽然能编译通过(因为有隐式转换),但可能会产生临时对象,或者在关联容器中因为类型不匹配而找不到。
- 必须传入
- 透明(C++14):
std::less<void>- 它可以接受任意类型的参数,只要这两个参数能被
operator<比较。 - 它内部直接调用
std::forward<T>(t) < std::forward<U>(u),不进行任何类型转换。
- 它可以接受任意类型的参数,只要这两个参数能被
1.1 为什么需要透明
C++14 中关于透明操作符函数对象的引入解决了我们在编程过程中的两大痛点:
避免不必要的类型转换
如果在算法中使用
std::sort,容器里的元素是int,但你提供的比较逻辑可能会涉及short或long。非透明版本可能会强制类型转换,产生临时对象。关联容器的高效查找(最重要)
这是 C++14 引入该特性的最直接原因:异构查找。
假设有一个
std::map<std::string, int>。在某些高性能场景下,想用const char*(C风格字符串)去查找key,而不想为了查找而临时构造一个std::string对象(这涉及内存分配)。C++11 (麻烦且低效):
1
2
3
4
5std::map<std::string, int> m = {{"hello", 1}};
// 必须构造临时 string 对象,产生内存分配开销
auto it = m.find(std::string("hello"));
// 进行隐式类型转换,将 const char* 转换为 std::string
// m.find("hello");C++14 (优雅且高效):
1
2
3
4
5
6// 使用 transparent comparator 的 map
// 注意:需要显式指定比较器为 std::less<>
std::map<std::string, int, std::less<>> m = {{"hello", 1}};
// 直接传 C 字符串,无需构造 std::string!性能更高
auto it = m.find("hello");
1.2 透明的本质实现原理
关于透明的本质我们可以简单地理解为,这些函数对象内部使用了 auto 和 decltype 推导。
如果我们要手写一个简单的透明比较器,大概是这样的:
1 |
|
原理解析:
- 模板化函数调用:
operator()是一个模板函数,它可以接受任意类型T和U。 - 自动类型推导:编译器根据传入的参数自动推导
T和U。 - 完美转发 (
std::forward):参数的引用属性(左值/右值)被保留,不产生多余的拷贝。 decltype尾返回类型:返回类型由operator<的计算结果决定。如果T和U不能比较,这段代码直接在编译期报错。using is_transparent = void;是透明的关键标识:标准库容器(如std::map,std::unordered_map)会检查比较器类里是否有is_transparent这个嵌套类型。如果有,容器就知道这个比较器允许异构查找,从而在函数中开启异形参数支持。
1.3 什么时候该用透明版本
- 应该使用透明版本的情况:
- 关联容器查找:当你定义
std::map或std::set时,强烈建议默认使用std::less<>等透明版本代替默认的std::less<T>。这不会损失性能,反而为以后可能的类型转换打开方便之门。 - 泛型编程:在写模板函数或算法时,如果需要比较两个可能不同但可比较的类型。
- 混合类型计算:比如
std::accumulate时,累加器类型和元素类型可能不同。
- 关联容器查找:当你定义
- 不需要使用透明版本的情况:
- 当明确知道类型,且希望编译器强制类型检查(非常少见,通常强制类型错误是好事)。
- 在某些极度受限的嵌入式环境中,如果模板实例化导致二进制体积增大(通常影响极小)。
从 C++14 开始,编写 C++ 代码涉及比较或排序逻辑时,请习惯性地加上 <>(例如 std::less<>, std::equal_to<>),它是现代 C++ 更高效且安全的写法。
2. 完整列表
所有标准的算术、比较和逻辑运算符在 C++14 中都增加了透明的 void 特化版本:
| 功能 | 普通版本 | 透明版本 (C++14) |
|---|---|---|
| 算术运算 | ||
| 加法 | std::plus |
std::plus<> |
| 减法 | std::minus |
std::minus<> |
| 乘法 | std::multiplies |
std::multiplies<> |
| 除法 | std::divides |
std::divides<> |
| 取模 | std::modulus |
std::modulus<> |
| 取反 | std::negate |
std::negate<> |
| 比较运算 | ||
| 等于 | std::equal_to |
std::equal_to<> |
| 不等于 | std::not_equal_to |
std::not_equal_to<> |
| 大于 | std::greater |
std::greater<> |
| 小于 | std::less |
std::less<> |
| 大于等于 | std::greater_equal |
std::greater_equal<> |
| 小于等于 | std::less_equal |
std::less_equal<> |
| 逻辑运算 | ||
| 逻辑与 | std::logical_and |
std::logical_and<> |
| 逻辑或 | std::logical_or |
std::logical_or<> |
| 逻辑非 | std::logical_not |
std::logical_not<> |
为了让大家清晰地理解 C++14 中透明操作符 (Operator<>) 与普通操作符 (Operator<T>) 的区别,下面将针对列表中的每一类操作符提供具体的代码示例。
2.1 算术运算
普通版本要求两个操作数类型必须一致(或者能隐式转换),而透明版本允许类型不同,只要它们能互相运算。
1 |
|
2.2 比较运算
这是最实用的部分。特别是结合 std::string 和 const char*,或者自定义类型之间的比较。
1 |
|
2.3 逻辑运算
逻辑运算通常隐式进行布尔转换。透明版本使得我们可以传入任意能被转换为布尔值的类型,甚至不需要强制类型转换。
1 |
|
在MyObject类中用到了自定义布尔转换,这是 C++11 引入的一个特性,用于定义一个显式的布尔类型转换。
1 | explicit operator bool() const { return value != 0; } |
这行代码的含义可以从以下几个部分详细拆解:
operator bool:
这是 C++ 中重载类型转换运算符的语法。它告诉编译器:当前类的对象可以转换成bool类型。const:
放在函数括号后面,表示这是一个const成员函数。这意味着它承诺不会修改类的成员变量(即它不会修改value)。explicit(关键字):
这是 C++11 及以后版本新增的关键字,用在这里表示“显式”。它的作用是禁止隐式类型转换,只允许显式调用。{ return value != 0; }:
这是转换的具体逻辑。当对象被转换为布尔值时,结果的真假取决于成员变量value是否不等于 0。
















