第2章:函数与lambda表达式变形
# 第2章:函数与lambda表达式变形
# 概述
本章主要聚焦于C++23中函数和lambda表达式的变形与适配,以应对不同类型的动态情况。首先,我们将探讨参数包(parameter packs)如何通过让函数处理数量不限的参数,来提升代码的灵活性和可复用性。接着,我们会讲解lambda表达式,以及它们在C++23中如何动态处理作为变量传递的参数。随后,我们将解释可变形lambda表达式(shape-shifting lambdas),这将教会你如何动态修改lambda表达式,以处理各种数据类型和数量的参数。
之后,我们会深入探讨更高级的函数重载方法,向你展示如何通过创建能处理不同类型参数和场景的重载函数来优化代码。最后,我们将深入研究std::function
和可调用对象(callable objects),它们能让你动态存储、管理和调用函数。在本章结束时,你将学会编写动态、适应性强的函数,这些函数能够处理多种参数类型,从而让你的代码更强大。
# 使用参数包变形函数
当一个函数“变形”时,它在运行时会变得更具动态性和适应性,能够处理各种类型和数量的输入。这个概念与前面提到的参数包所提供的灵活性密切相关,参数包使函数能够接受任意数量的参数。在C++23中,由于模板元编程(template metaprogramming)、完美转发(perfect forwarding)和参数包的发展,变形函数变得更强大、更顺畅。这些工具让我们能够构建高度可复用且灵活的应用程序编程接口(APIs),它们可以响应动态输入,而无需为同一个函数编写多个重载版本。
# 使用参数包动态变形函数
下面是一个简单示例,展示了如何使用参数包对函数进行动态变形。这个函数将打印传入参数的值:
#include <iostream>
// 用于打印多个参数的可变参数模板函数
template<typename... Args>
void print_args(Args... args) {
(std::cout << ... << args) << std::endl; // 用于打印所有参数的折叠表达式
}
int main() {
print_args(1, 2.5, "Hello", 'A'); // 输出: 1 2.5 Hello A
return 0;
}
2
3
4
5
6
7
8
9
10
在上述示例脚本中,print_args
函数使用参数包Args...
接受任意数量的参数。通过使用折叠表达式(std::cout << ... << args)
,它将输出流操作符<<
应用于参数包中的所有参数,并按顺序打印它们。该函数在运行时动态调整,以处理不同数量和类型的参数。
我们在这里创建了一个可变形函数,它会根据输入参数改变行为。这种技术让我们避免为处理不同数量的参数而编写同一个函数的多个重载版本,使代码更简洁、更具可复用性。
# 创建可复用且灵活的API
参数包的真正强大之处在于它能让API极具灵活性。通过将参数包与模板元编程和完美转发相结合,我们可以构建在保持类型安全和效率的同时,动态适应其行为的函数。
我们将基于前面的示例,创建一个更灵活的函数,以更有意义的方式处理每个参数。例如,我们可能想要一个函数,它打印每个参数,然后计算传递给它的所有数值的总和:
#include <iostream>
#include <type_traits>
// 用于打印单个参数的函数
template<typename T>
void process_arg(T arg) {
std::cout << "Processing: " << arg << std::endl;
}
// 用于处理参数并对数值求和的可变参数模板函数
template<typename... Args>
auto process_args(Args... args) {
(process_arg(args), ...); // 处理每个参数的折叠表达式
return (0 + ... + (std::is_arithmetic<Args>::value ? args : 0)); // 对数值求和
}
int main() {
auto sum = process_args(1, 2.5, "Hello", 42, 'A');
std::cout << "Sum of numeric values: " << sum << std::endl; // 输出: Sum of numeric values: 45.5
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
在上述示例脚本中,process_args
函数接受任意数量的参数,使用process_arg
函数(该函数只是打印参数)处理每个参数,然后计算所有数值的总和。我们使用类型特性库中的std::is_arithmetic
检查每个参数是否为数值类型。如果是,则将该值加到总和中;如果不是,则忽略它。这个函数具有很高的可复用性,因为它可以处理任意类型参数的组合。代码无需关心输入中包含的是字符串、整数还是其他类型——process_args
会自动调整,处理每个参数并对数值求和。这是一个可变形函数的明显示例:它会根据传入参数的类型动态改变行为。
# 使用参数包进行完美转发
为了进一步提高变形函数的灵活性和效率,我们可以引入完美转发。通过完美转发,我们确保参数以传递时的原样转发到目标函数,而不会进行不必要的复制。在处理复制成本较高的对象(如大型容器或自定义对象)时,这一点尤为重要。
为此,我们将修改process_args
函数,使其支持完美转发:
#include <iostream>
#include <type_traits>
#include <utility> // For std::forward
// 使用完美转发处理单个参数的函数
template<typename T>
void process_arg(T&& arg) {
std::cout << "Processing: " << std::forward<T>(arg) << std::endl;
}
// 具有完美转发的可变参数模板函数
template<typename... Args>
auto process_args(Args&&... args) {
(process_arg(std::forward<Args>(args)), ...); // 将每个参数转发到process_arg
return (0 + ... + (std::is_arithmetic_v<std::decay_t<Args>> ? args : 0)); // 对数值求和
}
int main() {
int x = 10;
auto sum = process_args(x, 2.5, "Hello", 42, 'A');
std::cout << "Sum of numeric values: " << sum << std::endl; // 输出: Sum of numeric values: 54.5
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
在这个更新版本中,process_arg
函数接受类型为T&&
的参数,这是一个万能引用(universal reference)。这使我们能够绑定到左值和右值。然后,我们使用std::forward
确保参数以传递时的原样转发,保留其原始类型和值类别。
process_args
函数现在对传递给它的所有参数使用完美转发(perfect forwarding)。这确保了无论参数是左值(如本示例中的x
)还是右值(如字面量42
),它们都能被高效地转发给process_arg
,而无需进行不必要的复制。
# 使用模板元编程(Template Metaprogramming)构建灵活的API
模板元编程与参数包(parameter packs)和完美转发相结合,使我们能够构建高度灵活且可复用的API。在这里,我们将构建一个简单的API,它可以处理异构数据列表,并根据参数的类型应用不同的操作。例如,我们可能希望以不同的方式处理数值和字符串:
#include <iostream>
#include <string>
#include <type_traits>
#include <utility>
// 处理数值参数
template<typename T>
std::enable_if_t<std::is_arithmetic_v<T>, void> process(T&& arg) {
std::cout << "Numeric value: " << arg << std::endl;
}
// 处理字符串参数
template<typename T>
std::enable_if_t<std::is_same_v<std::decay_t<T>, std::string>, void> process(T&& arg) {
std::cout << "String value: " << arg << std::endl;
}
// 具有完美转发的可变参数模板函数
template<typename... Args>
void process_all(Args&&... args) {
(process(std::forward<Args>(args)), ...); // 将每个参数转发到适当的重载函数
}
int main() {
process_all(42, 3.14, "Hello", 99); // 输出:Numeric value: 42, Numeric value: 3.14, String value: Hello, Numeric value: 99
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
在上述程序中,我们使用std::enable_if
和std::is_arithmetic
有选择地启用process
函数的不同重载版本。第一个重载版本处理数值,而第二个处理字符串。process_all
函数使用完美转发,确保每个参数都被传递到process
函数的适当重载版本。这个函数非常灵活,因为它可以处理数值和字符串值的任意组合,并为每种类型应用正确的逻辑。
# 使用可变参数Lambda表达式处理可变参数
# Lambda表达式简介
Lambda表达式是匿名函数,它使开发者能够编写更简洁、更灵活的代码。Lambda表达式通常用作临时函数,可以在不需要正式函数定义的情况下传递或即时执行。自C++11引入Lambda表达式以来,到了C++23,它变得更加动态和强大,提供了一种像参数包用于常规函数那样灵活处理可变参数的机制。Lambda表达式通常使用以下语法定义:
auto lambda = [](int a, int b) { return a + b; };
这个Lambda函数接受两个参数a
和b
,并返回它们的和。Lambda表达式很有用,因为它允许你就地定义小函数,使代码更易读、更灵活。然而,传统Lambda表达式的一个关键限制是,它们要求你预先指定参数的数量和类型。为了克服这个限制,可变参数Lambda表达式(shape-shifting lambdas)允许Lambda表达式像我们在常规函数中看到的参数包那样处理可变参数。可变参数Lambda表达式是指能够动态处理多种参数类型和数量的Lambda表达式。在C++23中,我们可以在Lambda表达式中使用参数包和折叠表达式,使它们能够处理任意数量的参数,使其像可变参数模板函数一样灵活。这种能力使Lambda表达式能够根据接收到的输入动态调整其行为,这对于复杂和动态的操作非常有用。
# 利用可变参数Lambda表达式
我们首先将Lambda表达式集成到现有的示例程序中。为此,我们将创建一个简单的Lambda表达式,它可以动态处理任意数量的参数。这类似于我们在函数中使用参数包的方式,但现在我们将相同的功能封装在一个Lambda表达式中。
下面是一个示例程序,展示了如何使用可变参数Lambda表达式打印任意数量的参数:
#include <iostream>
#include <utility> // 用于std::forward
int main() {
// 使用折叠表达式打印多个参数的Lambda函数
auto print_args = [](auto&&... args) {
(std::cout << ... << std::forward<decltype(args)>(args)) << std::endl;
};
// 使用不同类型的参数调用Lambda函数
print_args(1, 2.5, "Hello", 42, 'A'); // 输出:1 2.5 Hello 42 A
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
在上述程序中,我们定义了一个Lambda函数print_args
,它使用参数包auto&&... args
。这使得Lambda表达式可以接受任意数量、任意类型的参数。然后,我们使用折叠表达式打印每个参数,就像之前的函数示例一样。std::forward<decltype(args)>(args)
确保参数按原样转发(保留其类型和值类别)。
这个Lambda表达式的灵活性在于它能够在运行时处理不同类型和数量的参数。无论我们传递整数、浮点数、字符串还是字符,Lambda表达式都会动态调整以打印所有内容。这就是我们所说的可变参数:Lambda表达式根据接收到的参数改变其形式。
# 作为灵活动态可调用对象的Lambda表达式
Lambda表达式本质上是灵活的,因为它们是可调用对象。它们可以传递给其他函数、存储在变量中,并在以后调用。通过将参数包合并到Lambda表达式中,我们可以进一步增强它们的灵活性,使其成为处理动态参数的强大工具。
我们将通过根据参数类型以不同方式处理参数来增强我们的Lambda表达式。例如,我们可能希望以与字符串不同的方式打印数值。我们可以使用std::is_arithmetic
检查一个值是否为数值,然后在Lambda表达式中相应地应用不同的逻辑。
以下是如何修改Lambda表达式来实现这一点:
#include <iostream>
#include <string>
#include <type_traits>
#include <utility> // 用于std::forward
int main() {
// 动态处理多个参数的Lambda函数
auto process_args = [](auto&&... args) {
// 根据参数类型处理每个参数的折叠表达式
((std::is_arithmetic_v<std::decay_t<decltype(args)>> ? std::cout << "Numeric: " << args << std::endl :
std::cout << "Non-numeric: " << args << std::endl), ...);
};
// 使用混合参数类型调用Lambda函数
process_args(1, 2.5, "Hello", 42, 'A'); // 输出:Numeric: 1, Numeric: 2.5, Non-numeric: Hello, Numeric: 42, Non-numeric: A
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
在这个版本中,Lambda函数process_args
使用折叠表达式遍历参数。它使用std::is_arithmetic_v
检查每个参数的类型,std::is_arithmetic_v
是一个类型特性,用于检查参数是否为数值类型。如果参数是数值,Lambda表达式会打印“Numeric: ”,后面跟着参数值。否则,它会打印“Non-numeric: ”,后面跟着参数值。
这个示例展示了Lambda表达式如何可变:它们根据输入动态调整内部逻辑,似乎是处理混合数据类型的最佳解决方案。
# 可变参数Lambda表达式与其他可调用对象
Lambda表达式并不是C++中唯一的动态可调用对象。其他可调用对象,如函数指针、std::function
和仿函数(重载了operator()
的对象),也可以与Lambda表达式结合使用,以创建更灵活、更动态的系统。
例如,我们将把一个Lambda表达式与std::function
结合使用,std::function
允许我们存储任何可调用对象(包括Lambda表达式),并在以后调用它。以下是如何将std::function
与可变参数Lambda表达式结合使用:
#include <iostream>
#include <functional>
#include <string>
#include <type_traits>
int main() {
// 动态处理参数的Lambda函数
std::function<void(auto&&...)> process_args = [](auto&&... args) {
((std::is_arithmetic_v<std::decay_t<decltype(args)>> ? std::cout << "Numeric: " << args << std::endl :
std::cout << "Non-numeric: " << args << std::endl), ...);
};
// 调用存储在std::function中的Lambda函数
process_args(1, 2.5, "Hello", 42, 'A'); // 输出:Numeric: 1, Numeric: 2.5, Non-numeric: Hello, Numeric: 42, Non-numeric: A
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
在上述程序中,我们将Lambda函数process_args
存储在std::function
中。这使我们能够在程序的后面动态存储、传递和调用该Lambda表达式。std::function
使Lambda表达式更加灵活,因为它可以将Lambda表达式视为一等公民对象,在运行时可以存储、移动或重新赋值。
总体而言,可变参数Lambda表达式可以应用于许多实际场景,例如事件处理,在事件处理中不同的事件可能会传递不同数量和类型的数据。它们在函数式编程范式中也很有用,在这种范式中,函数通常作为参数传递或作为结果返回,而动态行为是核心需求。
# 通过函数重载(Function Overloading)优化代码
函数重载允许定义多个同名但参数类型或参数数量不同的函数。这使得我们能够构建类型安全且更灵活的动态解决方案,以适应不同的输入场景。在本节中,我们将通过将函数重载与可变参数模板(variadic templates)和完美转发相结合,将函数重载发挥到极致,创建强大且适应性强的代码。我们还将处理更复杂的问题,如歧义消除和重载解析,确保我们的重载函数在各种场景下都能正确运行。
在开始之前,我们先回顾一下函数重载的基础知识,然后再讨论更高级的应用。
# 基本函数重载
以下是一个基本形式的函数重载简单示例:
#include <iostream>
// 针对不同类型的重载函数
void print(int x) {
std::cout << "Integer: " << x << std::endl;
}
void print(double x) {
std::cout << "Double: " << x << std::endl;
}
void print(const std::string& x) {
std::cout << "String: " << x << std::endl;
}
int main() {
print(42); // 调用int类型的重载函数
print(3.14); // 调用double类型的重载函数
print("Hello"); // 调用string类型的重载函数
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
在上述脚本中,我们有三个print
函数,分别处理整数、双精度浮点数和字符串。编译器根据传递的参数类型来解析调用哪个版本。这就是函数重载的基本概念:允许存在多个同名但参数类型或参数数量不同的函数。然而,当你想要处理多种类型或复杂的参数集时,这种基本形式的函数重载很快就会变得繁琐。这就是可变参数模板、完美转发和高级函数重载发挥作用的地方。
# 结合可变参数模板与函数重载
现在,我们将创建一个更具动态性的函数,该函数能够使用可变参数模板(variadic templates)和函数重载(function overloading)来处理多种参数类型。在这种情况下,我们将进一步拓展函数重载的应用,创建一个可以处理不同类型的多个参数,并将它们恰当地转发到正确的重载函数的函数。
下面的示例展示了如何使用可变参数模板和完美转发(perfect forwarding)来动态地重载函数:
#include <iostream>
#include <string>
#include <utility> // 用于std::forward
// 针对不同类型的函数重载
void process(int x) {
std::cout << "Processing integer: " << x << std::endl;
}
void process(double x) {
std::cout << "Processing double: " << x << std::endl;
}
void process(const std::string& x) {
std::cout << "Processing string: " << x << std::endl;
}
// 带有完美转发的可变参数模板函数,用于处理多个参数
template<typename... Args>
void process_all(Args&&... args) {
(process(std::forward<Args>(args)), ...); // 将每个参数转发到正确的重载函数
}
int main() {
process_all(42, 3.14, "Hello"); // 为每个参数调用合适的重载函数
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
在上述示例脚本中,process_all
函数使用可变参数模板和完美转发来处理不同类型的多个参数。折叠表达式 (process(std::forward<Args>(args)), ...)
确保每个参数都能根据其类型被转发到合适的 process
重载函数。这使我们能够动态地处理不同的参数类型,而无需手动编写多个重载函数。
# 处理复杂的重载情况
虽然简单的重载很有用,但在实际应用中,常常会涉及更复杂的情况,即多个重载函数看起来都同样适用,从而导致歧义。在这种情况下,消除歧义并进行重载决议(overload resolution)对于确保调用正确的函数至关重要。我们先来看看可能产生歧义的情况,以及如何解决它。
#include <iostream>
#include <string>
// 针对不同类型的重载函数
void process(int x) {
std::cout << "Processing integer: " << x << std::endl;
}
void process(float x) {
std::cout << "Processing float: " << x << std::endl;
}
void process(double x) {
std::cout << "Processing double: " << x << std::endl;
}
int main() {
process(3.14); // 存在歧义:应该调用哪个重载函数?
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
在上述示例脚本中,当我们将值 3.14
传递给 process
函数时,在 float
和 double
的重载函数之间就产生了歧义。理论上,这两个重载函数都可以处理该参数,但编译器无法确定选择哪一个,从而导致歧义错误。
现在,为了解决这个歧义,我们可以使用显式类型转换,或者引入额外的重载函数,以确保调用正确的函数。下面是一种通过为字面量值添加更特定的重载函数来解决该问题的方法:
#include <iostream>
#include <string>
// 针对不同类型的重载函数
void process(float x) {
std::cout << "Processing float: " << x << std::endl;
}
void process(double x) {
std::cout << "Processing double: " << x << std::endl;
}
// 针对字面量值的特殊重载(字面量默认被视为double类型)
void process(long double x) {
std::cout << "Processing long double: " << x << std::endl;
}
int main() {
process(3.14); // 调用double类型的重载函数(因为3.14是一个double类型的字面量)
process(3.14f); // 调用float类型的重载函数(3.14f明确是一个float类型)
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
通过添加针对 long double
的重载函数,我们确保像 3.14
这样的字面量值能够正确地解析到 double
类型的重载函数。像 3.14f
这样进行显式类型转换,也有助于通过指定应调用哪个重载函数来消除歧义。
# 使用模板元编程进行重载决议
在更复杂的情况下,特别是在处理模板时,重载决议可能会变得更加微妙。例如,你可能会遇到这样的情况:基于某些类型特性(type traits),你希望优先选择某个重载函数。在这种情况下,模板元编程(template metaprogramming)可以指导并实现重载决议过程。
我们将扩展示例,使用 std::enable_if
和类型特性来处理重载决议。这将使我们能够根据传递的参数类型,有选择地启用某些重载函数。
#include <iostream>
#include <string>
#include <type_traits>
// 针对整数类型的重载
template<typename T>
std::enable_if_t<std::is_integral_v<T>, void> process(T x) {
std::cout << "Processing integral: " << x << std::endl;
}
// 针对浮点类型的重载
template<typename T>
std::enable_if_t<std::is_floating_point_v<T>, void> process(T x) {
std::cout << "Processing floating-point: " << x << std::endl;
}
// 针对字符串的重载
void process(const std::string& x) {
std::cout << "Processing string: " << x << std::endl;
}
int main() {
process(42); // 调用整数类型的重载函数
process(3.14); // 调用浮点类型的重载函数
process("Hello"); // 调用字符串类型的重载函数
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
在上述程序中,我们使用 std::enable_if
和类型特性来指导重载决议。std::is_integral_v<T>
和 std::is_floating_point_v<T>
检查确保根据参数是整数类型(如 int
或 long
)还是浮点类型(如 float
或 double
)来选择正确的重载函数。当编写必须处理多种类型,但又需要根据输入类型表现出不同行为的泛型代码时,这种技术特别有用。
# 利用完美转发构建类型安全的解决方案
为了进一步提高重载函数的灵活性,我们可以结合完美转发。完美转发确保参数在转发到合适的重载函数时,不会丢失其原始类型或值类别(value category)。
下面我们将看看如何将完美转发与函数重载相结合,创建类型安全且高效的解决方案:
#include <iostream>
#include <string>
#include <utility>
#include <type_traits>
// 针对整数的重载
template<typename T>
std::enable_if_t<std::is_integral_v<T>, void> process(T&& x) {
std::cout << "Processing integral: " << std::forward<T>(x) << std::endl;
}
// 针对浮点数的重载
template<typename T>
std::enable_if_t<std::is_floating_point_v<T>, void> process(T&& x) {
std::cout << "Processing floating-point: " << std::forward<T>(x) << std::endl;
}
// 针对字符串的重载
void process(const std::string& x) {
std::cout << "Processing string: " << x << std::endl;
}
int main() {
int x = 42;
process(x); // 调用整数类型的重载函数,左值(lvalue)
process(3.14); // 调用浮点类型的重载函数,右值(rvalue)
process("Hello"); // 调用字符串类型的重载函数,右值
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
在这个版本中,我们使用完美转发(std::forward<T>(x)
)来确保参数在不进行不必要复制的情况下,被转发到正确的重载函数。std::enable_if
检查确保根据参数类型选择正确的重载函数,而完美转发则保留了值类别(无论是左值还是右值)。
在复杂的系统中,特别是当涉及多层模板和类型特性时,重载决议可能会变得棘手。在这种情况下,设计出足够健壮的函数很重要,这些函数要能够处理潜在的歧义,同时保持类型安全性和灵活性。
# 使用std::function
和可调用对象实现动态函数
可调用对象(Callable objects)包括任何可以像函数一样被 “调用” 的东西,这其中有普通函数、函数指针、lambda表达式以及仿函数(重载了operator()
的对象)。std::function
类模板提供了一个灵活的包装器,它可以容纳任何可调用对象,使函数能够被动态地传递、存储和调用。当你想要设计在运行时能够自适应的代码时,这种灵活性就显得尤为有用,因为它让你可以创建动态且高度可复用的函数。
在本节中,我们将探讨如何使用std::function
和可调用对象来构建动态函数,这些函数可以作为参数传递、存储在容器中,并在各种场景下被调用 。
# std::function
简介
std::function
是一个多态函数包装器,这意味着它可以存储任何与特定函数签名匹配的可调用实体。这使得它在动态编程中非常通用,在动态编程中,你可能需要根据上下文存储和调用不同的函数。
为了实际了解它的用法,我们从一个简单的示例开始,展示如何使用std::function
来存储和调用一个lambda函数:
#include <iostream>
#include <functional> // For std::function
int main() {
// 定义一个lambda函数
auto lambda = [](int x, int y) -> int {
return x + y;
};
// 将lambda存储在std::function中
std::function<int(int, int)> add = lambda;
// 调用std::function
std::cout << "Result: " << add(3, 4) << std::endl;
// 输出:Result: 7
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
在上面的示例代码中,我们定义了一个简单的lambda函数,它接受两个整数并返回它们的和。然后,我们将这个lambda存储在一个名为add
的std::function
对象中,add
被定义为与lambda的签名匹配:int(int, int)
。一旦lambda被存储在std::function
中,就可以像调用普通函数一样调用它。
std::function
的主要优点是它能够容纳任何可调用实体,而不仅仅是lambda表达式。这包括普通函数、函数指针和仿函数。
# 存储和传递动态函数
std::function
的灵活性使你能够在运行时动态地在不同函数之间进行切换。我们将扩展前面的示例,引入一个普通函数,并展示如何将一个函数和一个lambda都存储在同一个std::function
中,并动态地传递。
#include <iostream>
#include <functional>
// 一个普通函数
int multiply(int x, int y) {
return x * y;
}
int main() {
// lambda函数
auto add = [](int x, int y) -> int {
return x + y;
};
// std::function可以存储普通函数和lambda
std::function<int(int, int)> operation = add;
std::cout << "Addition: " << operation(3, 4) << std::endl;
// 输出:Addition: 7
// 切换到普通函数
operation = multiply;
std::cout << "Multiplication: " << operation(3, 4) << std::endl;
// 输出:Multiplication: 12
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
在上面的示例代码中,我们在同一个std::function
中存储了两种不同类型的可调用对象(一个lambda和一个普通函数)。程序首先将lambda add
存储在std::function
中,并调用它来执行加法运算。然后,我们将存储的函数切换为普通的multiply
函数,并调用它来执行乘法运算。
# 结合std::function
使用仿函数
仿函数(Functors),也称为函数对象,是那些可以像函数一样被调用的对象。它们通过在类或结构体中重载operator()
来实现这一点。std::function
也可以存储仿函数,这提供了另一层灵活性。
下面是一个如何结合std::function
使用仿函数的示例:
#include <iostream>
#include <functional>
// 一个执行减法运算的仿函数(函数对象)
struct Subtract {
int operator()(int x, int y) const {
return x - y;
}
};
int main() {
// 定义一个仿函数对象
Subtract subtract;
// 将仿函数存储在std::function中
std::function<int(int, int)> operation = subtract;
std::cout << "Subtraction: " << operation(10, 4) << std::endl;
// 输出:Subtraction: 6
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
在上面的示例代码中,我们定义了一个仿函数Subtract
,它重载了operator()
以执行减法运算。然后,我们将这个仿函数存储在一个std::function
中,并像调用任何其他函数一样调用它。这展示了std::function
的灵活性,因为它除了可以无缝处理lambda和普通函数之外,还可以处理仿函数。
# 将函数作为参数传递
std::function
最常见的用法之一是将函数作为参数传递给其他函数,这使得动态行为能够被注入到程序的不同部分。我们将创建一个示例,在这个示例中,我们将一个函数作为参数传递给另一个函数:
#include <iostream>
#include <functional>
// 一个接受可调用对象的高阶函数
void execute_operation(const std::function<int(int, int)>& func, int a, int b) {
std::cout << "Result: " << func(a, b) << std::endl;
}
int main() {
// 执行除法运算的lambda函数
auto divide = [](int x, int y) -> int {
return y != 0 ? x / y : 0;
// 简单除法运算,检查是否除以零
};
// 使用lambda调用高阶函数
execute_operation(divide, 20, 4);
// 输出:Result: 5
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
在上面的程序中,execute_operation
函数接受一个std::function<int(int, int)>
作为参数,允许传递任何具有该签名的可调用对象。我们将一个执行除法运算的lambda传递给execute_operation
,并且该lambda在函数内部被动态调用。这种方法使代码更加灵活和可复用,因为你可以在不改变execute_operation
内部实现的情况下,向它传递不同的函数。
# 将函数存储在容器中
std::function
的另一个优点是它可以存储在容器中,这使你能够构建可调用对象的列表或映射。在需要存储一组函数并根据条件动态调用它们的场景中,这特别有用。
下面是一个示例,我们在一个容器中存储多个std::function
对象并调用它们:
#include <iostream>
#include <functional>
#include <vector>
// 一组简单的运算函数
int add(int x, int y) {
return x + y;
}
int subtract(int x, int y) {
return x - y;
}
int multiply(int x, int y) {
return x * y;
}
int main() {
// 定义一个std::function对象的向量
std::vector<std::function<int(int, int)>> operations;
// 将不同的运算函数存储在容器中
operations.push_back(add);
operations.push_back(subtract);
operations.push_back(multiply);
// 使用相同的参数执行所有存储的运算函数
for (const auto& operation : operations) {
std::cout << "Result: " << operation(10, 5) << std::endl;
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
在上面的程序中,我们定义了三个函数(add
、subtract
和multiply
),并将它们存储在一个std::function
的std::vector
中。然后,我们遍历容器,使用相同的参数调用每个函数,这展示了如何使用std::function
创建可根据情况动态调用的可调用对象集合。
# std::function
与成员函数
除了处理自由函数、lambda 表达式和仿函数(functor)之外,std::function
还可以存储类的成员函数。在存储成员函数时,需要同时传递对象实例和成员函数指针。
以下是一个关于如何在 std::function
中使用成员函数的示例程序:
#include <iostream>
#include <functional>
class Calculator {
public:
int add(int x, int y) const {
return x + y;
}
int multiply(int x, int y) const {
return x * y;
}
};
int main() {
Calculator calc;
// 将成员函数存储在 std::function 中
std::function<int(const Calculator&, int, int)> operation;
// 存储并调用 add 成员函数
operation = &Calculator::add;
std::cout << "Addition: " << operation(calc, 3, 4) << std::endl; // 输出:Addition: 7
// 存储并调用 multiply 成员函数
operation = &Calculator::multiply;
std::cout << "Multiplication: " << operation(calc, 3, 4) << std::endl; // 输出:Multiplication: 12
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
在上述示例代码中,我们将 Calculator
类的成员函数 add
和 multiply
存储在 std::function
中。在调用成员函数时,我们将对象 calc
传递给 std::function
。这种技术允许你动态地存储和调用成员函数,进一步增强了 std::function
的灵活性。无论你处理的是 lambda 表达式、常规函数、仿函数还是成员函数,std::function
都为处理所有类型的可调用对象提供了统一的接口。
# 总结
总的来说,目标是让 C++23 中的函数更具动态性和灵活性。在本章开头,我们探讨了如何利用参数包(parameter packs)来改变函数的形态,使其能够轻松处理参数数量不同的函数。通过示例展示了如何将可变参数模板(variadic templates)与完美转发(perfect forwarding)相结合,为各种数据类型和数量构建适应性强的应用程序编程接口(API)。接下来,我们将注意力转向 lambda 表达式,首次了解到了可变形 lambda(shape - shifting lambdas)。这些 lambda 表达式能够动态地处理不同类型的参数,进一步提高了匿名函数的灵活性。示例展示了 lambda 表达式对于动态操作是多么有用,因为它们是可以处理不同类型参数的可调用对象。
本章最后探讨了高级函数重载,展示了如何扩展这项技术来处理诸如歧义(ambiguity)和消歧(disambiguation)等复杂情况。展示了使用类型特性(type traits)和其他技术进行重载决议(overload resolution),为开发动态、类型安全的解决方案铺平了道路。它还通过对 std::function
和可调用对象的详细介绍,展示了如何构建具有在运行时传递、存储和调用函数能力的动态、灵活的系统。本章涵盖了处理函数相关的所有方面,让你具备编写能够即时变化和适应的代码的技能。