1. 什么是函数对象 (Function Object)?
首先,一个函数对象(也常被称为仿函数 Functor),其本质是一个重载了函数调用运算符 operator() 的类的对象。
简单来说,它是一个“行为像函数”的对象。你可以像调用一个普通函数一样来“调用”这个对象。
一个最简单的自定义函数对象示例:
#include <iostream>
// 1. 定义一个类
class Greeter {
public:
// 2. 在类中重载 operator()
void operator()(const std::string& name) const {
std::cout << "Hello, " << name << "!" << std::endl;
}
};
int main() {
Greeter greet_object; // 3. 创建这个类的对象
// 4. 像调用函数一样“调用”这个对象
greet_object("World"); // 输出: Hello, World!
}
在这个例子中,greet_object 就是一个函数对象。
函数对象最大的优势:
- 可以拥有状态:因为函数对象是类的对象,所以它可以有自己的成员变量来存储状态。这是普通函数做不到的。
- 性能可能更高:编译器更容易对函数对象进行内联优化,通常比通过函数指针调用要快。
2. 什么是“标准库”函数对象?
C++标准库(主要在头文件 <functional> 中)为我们预先定义好了一系列常用的、开箱即用的函数对象。这样我们就不用自己去手写 std::plus、std::greater 这些简单的操作了。
这些标准库函数对象主要用于配合STL算法(如 std::sort, std::transform 等)使用,让代码更加简洁和标准化。
C++20 <ranges> 中的函数对象
这些是 C++20 引入的现代版本,它们默认是“透明的”(可以处理不同但可比较的类型),并且与范围库无缝集成。
| 类别 | 函数对象 (Function Object) | 功能说明 | 等价操作 |
|---|---|---|---|
| 比较 | std::ranges::equal_to |
判断 a 是否等于 b |
a == b |
std::ranges::not_equal_to |
判断 a 是否不等于 b |
a != b |
|
std::ranges::less |
判断 a 是否小于 b |
a < b |
|
std::ranges::less_equal |
判断 a 是否小于或等于 b |
a <= b |
|
std::ranges::greater |
判断 a 是否大于 b |
a > b |
|
std::ranges::greater_equal |
判断 a 是否大于或等于 b |
a >= b |
|
| 其他 | std::ranges::identity |
返回其参数本身,不做任何改变 | x |
std::ranges::identity 的特殊用途: 它在需要“投影”(Projection)但又不想改变元素的算法中非常有用。例如,在一个需要投影的 sort 算法中,如果你想按元素自身的值排序,就可以使用 std::ranges::identity作为投影。
<functional> 中的传统函数对象
这些是 C++ 标准库早期版本提供的函数对象。它们功能强大,至今仍在广泛使用。
1. 算术运算 (Arithmetic Operations)
| 函数对象 (Function Object) | 功能说明 | 等价操作 |
|---|---|---|
std::plus |
计算 a + b |
a + b |
std::minus |
计算 a - b |
a - b |
std::multiplies |
计算 a * b |
a * b |
std::divides |
计算 a / b |
a / b |
std::modulus |
计算 a % b |
a % b |
std::negate |
计算 -a (一元操作) |
-a |
2. 比较运算 (Comparison Operations)
| 函数对象 (Function Object) | 功能说明 | 等价操作 |
|---|---|---|
std::equal_to |
判断 a == b |
a == b |
std::not_equal_to |
判断 a != b |
a != b |
std::less |
判断 a < b |
a < b |
std::less_equal |
判断 a <= b |
a <= b |
std::greater |
判断 a > b |
a > b |
std::greater_equal |
判断 a >= b |
a >= b |
注意:从 C++14 开始,<functional> 中的比较和算术运算符可以通过使用空模板参数(如 std::less<>)来实现“透明性”,这使其能力接近于 <ranges> 中的版本。
3. 逻辑运算 (Logical Operations)
| 函数对象 (Function Object) | 功能说明 | 等价操作 |
|---|---|---|
std::logical_and |
计算 a && b |
a && b |
std::logical_or |
计算 `a | |
std::logical_not |
计算 !a (一元操作) |
!a |
4. 位运算 (Bitwise Operations)
| 函数对象 (Function Object) | 功能说明 | 等价操作 |
|---|---|---|
std::bit_and |
计算 a & b |
a & b |
std::bit_or |
计算 `a | b` |
std::bit_xor |
计算 a ^ b |
a ^ b |
std::bit_not |
计算 ~a (一元操作) |
~a |
3.统一初始化 (Uniform Initialization)
什么是统一初始化?
统一初始化,也称为列表初始化 (List Initialization) 或 花括号初始化 (Brace Initialization),是C++11引入的一种全新的、通用的初始化语法。它的核心就是使用花括号 {} 来初始化任意类型的对象。
这个语法旨在提供一种单一、无歧义的方式来初始化变量,无论它是普通的基本类型、数组、还是类的对象。
基本语法形式:
T object {arg1, arg2, ...};
T object = {arg1, arg2, ...}; // = 号是可选的
C++11之前的问题
在C++11之前,对象的初始化语法非常混乱,甚至会引发一些诡异的问题。
混乱的初始化方法:
对于聚合类型(struct/class)
struct Point {
int x, y;
};
Point p = {10, 20}; // 使用花括号
对于类对象:
#include <string>
#include <vector>
// 使用小括号调用构造函数
std::string s("hello");
std::vector<int> v(5, 10); // 5个元素,值都为10
对于普通变量和数组:
int x = 5;
int arr[] = {1, 2, 3}; // 使用花括号
需要根据不同的类型,记忆使用 =、() 还是 {},非常不统一。
统一初始化的好处
使用 {} 的统一初始化语法,不仅统一了风格,还带来了几个非常重要的好处:
统一初始化场景
现在,几乎所有初始化场景都可以使用 {},代码风格更加一致,降低了学习和记忆成本。
使用统一初始化后的代码:
// 普通变量
int x{5};
double pi{3.14};
// 数组
int arr[]{1, 2, 3};
// 类对象
std::string s{"hello"};
Point p{10, 20};
// 动态分配的对象
int* p_int = new int{10};
std::string* p_str = new std::string{"world"};
// 容器
std::vector<int> v{1, 2, 3, 4, 5};
可防止“窄化转换” (Narrowing Conversion)
“窄化转换”指的是一种可能丢失数据精度的隐式类型转换,比如把 double 赋值给 int,或者把 int 赋值给 char。在旧的初始化语法中,这种转换是合法的,但可能隐藏bug。
旧语法的风险:
double pi = 3.14159;
int x = pi; // 合法,但pi的小数部分被截断,x的值为3。编译器可能只给一个警告。
int y(pi); // 同上,y的值为3。
统一初始化的安全性: 统一初始化禁止窄化转换,如果发生这类转换,代码将无法通过编译。
double pi = 3.14159;
// int x{pi}; // 编译错误!不允许从 double 到 int 的窄化转换
// int y = {pi}; // 同样编译错误!
这个特性极大地提高了代码的健壮性和安全性,能在编译阶段就发现潜在的数据丢失问题。
解决歧义
这是C++中一个经典的语法歧义问题。看下面的代码:
MyClass obj();
你可能是想:用 MyClass 的默认构造函数创建一个名为 obj 的对象。 但C++编译器会认为:你声明了一个名为 obj 的函数,这个函数不接受任何参数,返回一个 MyClass 类型的对象。
这是一个让无数C++初学者困惑的陷阱。而统一初始化完美地解决了这个问题。
使用统一初始化解决:
MyClass obj{}; // 毫无歧义!这绝对是创建一个对象。
编译器看到 {} 就知道这一定是在定义一个变量,而不是声明一个函数。
注意:std::initializer_list
统一初始化有一个非常重要的规则,也是它和 () 初始化的最大区别所在:
如果一个类同时有“普通构造函数”和“以std::initializer_list为参数的构造函数”,那么使用{}进行初始化时,编译器会强烈优先选择std::initializer_list版本的构造函数。
最典型的例子就是 std::vector:
#include <vector>
#include <iostream>
int main() {
// 使用小括号 () -> 调用普通构造函数
// 创建一个包含10个元素的vector,每个元素的值都是5
std::vector<int> v1(10, 5);
// 使用花括号 {} -> 优先调用 initializer_list 构造函数
// 创建一个包含2个元素的vector,这两个元素分别是10和5
std::vector<int> v2{10, 5};
std::cout << "v1 size: " << v1.size() << std::endl; // 输出: v1 size: 10
std::cout << "v2 size: " << v2.size() << std::endl; // 输出: v2 size: 2
}
结论:
- 当你希望用花括号里的内容作为元素列表来初始化容器时,用
{}。 - 当你希望调用非
initializer_list的普通构造函数(比如指定大小和初始值)时,请继续使用()。
4.类模板参数推导 (CTAD)
我们可以注意到 std::ranges::equal_to 在使用时候没有加 < > ,而老的比较运算 std::equal_to<int> 往往要加上 < >。这是一个从C++14到C++17做出的便利性变化。
函数模板
在C++中,其实编译器一直在做“模板参数推导”,只是以前它主要用于函数模板。
比如,当写下这样的代码:
#include <utility>
// make_pair 是一个函数模板
auto p = std::make_pair(10, "hello");
从来不需要写成 std::make_pair<int, const char*>(10, "hello")。编译器会根据你传入的参数 10 和 "hello",自动推导出模板参数应该是 int 和 const char*。
C++17 :类模板参数推导 (CTAD)
在C++17之前,这种自动推导的功能对类模板是不起作用的。如果你想创建一个 std::pair 对象,你必须明确地写出类型:
C++17 之前:
// 必须显式提供模板参数 <int, const char*>
std::pair<int, const char*> my_pair(10, "hello");
C++17 之后 (有了CTAD): 编译器现在也可以根据构造函数的参数来推导类模板的参数了!
// 无需提供尖括号,编译器会自动推导!
std::pair my_pair(10, "hello"); // 编译器知道这是 std::pair<int, const char*>
这就是CTAD的核心。
应用到 std::ranges::equal_to
std::equal_to 和 std::ranges::equal_to 的定义本质上都是一个类模板,我们可以把它简化理解成这样:
template <typename T = void> // 注意这里有一个默认模板参数 void
struct equal_to {
// ... 内部实现 ...
// 它有一个默认的构造函数 equal_to() {}
};
注意这个 T = void,它是一个默认模板参数。当你不想指定具体类型(比如 int),而是想让它能比较任意类型时,就使用这个 void 版本(也称为“透明的函数对象”)。
现在我们来看演化过程:
- C++17/20 的做法 (
std::ranges::equal_to{}) 有了CTAD之后,当你写下std::ranges::equal_to{}时,编译器会进行推导:- 编译器看到
std::ranges::equal_to是一个类模板。 - 你正在调用它的默认构造函数(因为
{}是空的)。 - 构造函数没有任何参数,无法帮助推导
T的类型。 - 于是,编译器会去查找这个模板有没有默认模板参数。
- 它找到了!默认参数是
void。 - 因此,编译器自动推导出你想要的是
std::ranges::equal_to<void>。
- 编译器看到
C++14 的做法 (std::equal_to<>) C++14引入了透明函数对象,但当时还没有CTAD。所以,为了创建一个void版本的对象,你必须提供一个空的尖括号 <> 来告诉编译器采用默认参数。
// 必须有<>,这是在C++14/11中实例化一个模板类对象的语法
auto op = std::equal_to<>{};
所以,std::ranges::equal_to{} 就等价于 std::ranges::equal_to<void>{}。所以在C++之后,这些尖括号均不必添加。
总结
| 语法 (Syntax) | 含义 (Meaning) | 所需C++版本 (Required C++ Version) |
|---|---|---|
std::equal_to<int>{} |
显式指定类型为int的函数对象 |
C++11 |
std::equal_to<>{} |
显式使用默认模板参数(void),创建透明函数对象 |
C++14 |
std::equal_to{} |
使用CTAD自动推导出默认模板参数(void) |
C++17 |
std::ranges::equal_to{} |
使用CTAD自动推导出默认模板参数(void) |
C++20 |