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 |