好的,各位观众老爷,欢迎来到今天的“C++自定义std::function
:类型擦除背后的秘密”讲座!今天咱们不讲那些虚头巴脑的概念,直接撸起袖子干,把std::function
扒个精光,看看它到底是怎么玩类型擦除的。
一、std::function
:一个万能胶?
首先,咱们来认识一下std::function
。这玩意儿就像编程界的万能胶,可以粘合任何可调用对象,比如普通函数、lambda表达式、函数对象等等。
#include <iostream>
#include <functional>
int add(int a, int b) {
return a + b;
}
struct Multiply {
int operator()(int a, int b) {
return a * b;
}
};
int main() {
std::function<int(int, int)> func1 = add;
std::function<int(int, int)> func2 = [](int a, int b) { return a - b; };
std::function<int(int, int)> func3 = Multiply();
std::cout << "add: " << func1(5, 3) << std::endl; // 输出 8
std::cout << "lambda: " << func2(5, 3) << std::endl; // 输出 2
std::cout << "Multiply: " << func3(5, 3) << std::endl; // 输出 15
return 0;
}
你看,std::function<int(int, int)>
可以接受add
函数、lambda表达式,甚至Multiply
函数对象,只要它们的签名是int(int, int)
就行。 这简直太方便了!但是,问题来了:std::function
怎么知道这些可调用对象的信息? 它们类型各不相同,内部实现也千差万别,std::function
是怎么做到一视同仁的? 这就是类型擦除的功劳。
二、类型擦除:障眼法大师
类型擦除,简单来说,就是隐藏具体类型的信息,让使用者只关心接口,而不用关心底层实现。 就像你用遥控器控制电视,你只需要知道按哪个按钮可以换台、调节音量,而不需要知道电视内部的电路是如何工作的。
std::function
的核心思想就是利用虚函数和模板来实现类型擦除。它会创建一个“类型擦除外壳”,将各种可调用对象包装起来,然后通过虚函数来调用它们。
三、自定义MyFunction
:手撕类型擦除
为了彻底理解std::function
的实现原理,咱们来自己动手写一个简化版的MyFunction
。
3.1 基类:FunctionWrapper
首先,我们需要一个基类FunctionWrapper
,它是一个抽象类,定义了虚函数invoke
,用于实际调用可调用对象。
#include <iostream>
#include <memory>
template <typename Signature>
class FunctionWrapper; // 前置声明
template <typename R, typename... Args>
class FunctionWrapper<R(Args...)> {
public:
virtual R invoke(Args... args) = 0;
virtual ~FunctionWrapper() = default; // 虚析构函数
// 为了避免切片问题,我们需要拷贝构造和拷贝赋值
virtual std::unique_ptr<FunctionWrapper<R(Args...)>> clone() const = 0;
};
template <typename R, typename... Args>
:这是一个模板类,R
是返回值类型,Args...
是参数类型。virtual R invoke(Args... args) = 0;
:纯虚函数invoke
,用于实际调用可调用对象。 注意,这里使用了参数包Args...
,可以支持任意数量的参数。virtual ~FunctionWrapper() = default;
:虚析构函数,保证在销毁MyFunction
对象时,能够正确地销毁派生类的对象。 这个非常重要,否则可能会造成内存泄漏。virtual std::unique_ptr<FunctionWrapper<R(Args...)>> clone() const = 0;
:纯虚函数clone
,用于创建当前对象的副本。这是实现拷贝构造和拷贝赋值的关键。使用std::unique_ptr
来管理克隆对象的生命周期。
3.2 派生类:FunctionCaller
接下来,我们需要一个派生类FunctionCaller
,它继承自FunctionWrapper
,用于包装具体的可调用对象。
template <typename R, typename... Args>
template <typename F>
class FunctionWrapper<R(Args...)>::FunctionCaller : public FunctionWrapper<R(Args...)> {
public:
FunctionCaller(F&& func) : func_(std::forward<F>(func)) {}
R invoke(Args... args) override {
return func_(std::forward<Args>(args)...);
}
std::unique_ptr<FunctionWrapper<R(Args...)>> clone() const override {
return std::make_unique<FunctionCaller>(func_); // 创建副本
}
private:
F func_; // 存储可调用对象
};
template <typename F>
:这是一个模板类,F
是可调用对象的类型。FunctionCaller(F&& func) : func_(std::forward<F>(func)) {}
:构造函数,使用完美转发将可调用对象存储到func_
成员变量中。R invoke(Args... args) override
:重写invoke
函数,实际调用存储的可调用对象func_
。 这里也使用了完美转发,将参数传递给可调用对象。std::unique_ptr<FunctionWrapper<R(Args...)>> clone() const override
:重写clone
函数,创建当前FunctionCaller
对象的副本。使用std::make_unique
创建一个指向新FunctionCaller
对象的std::unique_ptr
。F func_;
:成员变量,用于存储可调用对象。
3.3 MyFunction
类
现在,我们可以创建MyFunction
类了,它负责管理FunctionWrapper
对象。
template <typename Signature>
class MyFunction; // 前置声明
template <typename R, typename... Args>
class MyFunction<R(Args...)> {
public:
MyFunction() : func_wrapper_(nullptr) {}
template <typename F>
MyFunction(F&& func) : func_wrapper_(std::make_unique<typename FunctionWrapper<R(Args...)>::FunctionCaller<F>>(std::forward<F>(func))) {}
MyFunction(const MyFunction& other) {
if (other.func_wrapper_) {
func_wrapper_ = other.func_wrapper_->clone(); // 克隆
} else {
func_wrapper_ = nullptr;
}
}
MyFunction& operator=(const MyFunction& other) {
if (this != &other) {
if (other.func_wrapper_) {
func_wrapper_ = other.func_wrapper_->clone(); // 克隆
} else {
func_wrapper_ = nullptr;
}
}
return *this;
}
~MyFunction() = default;
R operator()(Args... args) {
if (func_wrapper_) {
return func_wrapper_->invoke(std::forward<Args>(args)...);
} else {
throw std::runtime_error("MyFunction is empty");
}
}
explicit operator bool() const {
return func_wrapper_ != nullptr;
}
private:
std::unique_ptr<FunctionWrapper<R(Args...)>> func_wrapper_;
};
MyFunction() : func_wrapper_(nullptr) {}
:默认构造函数,将func_wrapper_
初始化为空指针。template <typename F> MyFunction(F&& func) : func_wrapper_(std::make_unique<typename FunctionWrapper<R(Args...)>::FunctionCaller<F>>(std::forward<F>(func))) {}
:带参数的构造函数,使用完美转发将可调用对象包装到FunctionCaller
中,然后将FunctionCaller
对象存储到func_wrapper_
中。MyFunction(const MyFunction& other)
:拷贝构造函数,用于创建MyFunction
对象的副本。 通过调用clone
方法来复制FunctionWrapper
对象,避免多个MyFunction
对象共享同一个FunctionWrapper
对象。MyFunction& operator=(const MyFunction& other)
:拷贝赋值运算符,用于将一个MyFunction
对象赋值给另一个MyFunction
对象。同样,通过调用clone
方法来复制FunctionWrapper
对象。~MyFunction() = default;
:析构函数,由于使用了std::unique_ptr
管理func_wrapper_
,因此不需要手动释放内存。R operator()(Args... args)
:重载函数调用运算符,用于实际调用存储的可调用对象。 如果func_wrapper_
为空指针,则抛出一个异常。explicit operator bool() const
:显式类型转换运算符,用于判断MyFunction
对象是否为空。std::unique_ptr<FunctionWrapper<R(Args...)>> func_wrapper_;
:成员变量,用于存储FunctionWrapper
对象。 使用std::unique_ptr
可以自动管理FunctionWrapper
对象的生命周期。
3.4 使用MyFunction
现在,我们可以使用MyFunction
了。
#include <iostream>
#include <stdexcept>
int add(int a, int b) {
return a + b;
}
struct Multiply {
int operator()(int a, int b) {
return a * b;
}
};
int main() {
MyFunction<int(int, int)> func1 = add;
MyFunction<int(int, int)> func2 = [](int a, int b) { return a - b; };
MyFunction<int(int, int)> func3 = Multiply();
std::cout << "add: " << func1(5, 3) << std::endl; // 输出 8
std::cout << "lambda: " << func2(5, 3) << std::endl; // 输出 2
std::cout << "Multiply: " << func3(5, 3) << std::endl; // 输出 15
MyFunction<int(int, int)> func4 = func1; // 拷贝构造
std::cout << "Copy: " << func4(10, 2) << std::endl; // 输出 12
MyFunction<int(int, int)> func5;
func5 = func2; // 拷贝赋值
std::cout << "Assignment: " << func5(10, 2) << std::endl; // 输出 8
if (func5) {
std::cout << "func5 is not empty" << std::endl;
}
MyFunction<int(int, int)> func6;
if (!func6) {
std::cout << "func6 is empty" << std::endl;
}
try {
func6(1, 2); // 调用空的MyFunction会抛出异常
} catch (const std::runtime_error& e) {
std::cerr << "Exception: " << e.what() << std::endl;
}
return 0;
}
这段代码演示了MyFunction
的基本用法,包括:
- 使用
MyFunction
包装普通函数、lambda表达式和函数对象。 - 调用
MyFunction
对象。 - 拷贝构造和拷贝赋值。
- 判断
MyFunction
对象是否为空。 - 调用空的
MyFunction
对象会抛出异常。
四、类型擦除机制总结
通过上面的例子,我们可以总结一下std::function
(以及我们自定义的MyFunction
)的类型擦除机制:
- 基类
FunctionWrapper
: 定义了统一的接口invoke
,用于调用可调用对象。它是一个抽象类,不能直接实例化。 - 派生类
FunctionCaller
: 模板类,用于包装具体的可调用对象。它继承自FunctionWrapper
,并实现了invoke
函数,实际调用存储的可调用对象。 MyFunction
类: 负责管理FunctionWrapper
对象。 它使用std::unique_ptr
来管理FunctionWrapper
对象的生命周期。它还重载了函数调用运算符,用于实际调用存储的可调用对象。- 虚函数:
FunctionWrapper
中的invoke
函数是虚函数,这使得我们可以通过基类指针来调用派生类的invoke
函数,从而实现多态。 - 模板:
FunctionCaller
是一个模板类,这使得我们可以包装任意类型的可调用对象。 - 完美转发: 使用完美转发可以将参数传递给可调用对象,而无需关心参数的类型。
- 拷贝构造和拷贝赋值: 通过
clone
方法来复制FunctionWrapper
对象,避免多个MyFunction
对象共享同一个FunctionWrapper
对象。
用表格来总结一下:
组件 | 功能 |
---|---|
FunctionWrapper |
定义统一的调用接口 ( invoke ),是抽象基类。包含一个纯虚函数 clone 用于创建对象副本。提供虚析构函数。 |
FunctionCaller |
模板类,用于包装具体的可调用对象。 实现 invoke 函数,实际调用存储的可调用对象。 实现 clone 函数,创建当前对象的副本。 |
MyFunction |
管理 FunctionWrapper 对象。 提供构造函数、拷贝构造函数、拷贝赋值运算符和析构函数。 重载函数调用运算符,用于调用存储的可调用对象。 使用 std::unique_ptr 管理 FunctionWrapper 对象的生命周期。 提供显式类型转换运算符,用于判断对象是否为空。 |
虚函数 | FunctionWrapper 中的 invoke 和 clone 是虚函数,允许通过基类指针调用派生类的实现,实现多态。 |
模板 | FunctionCaller 是模板类,允许包装任意类型的可调用对象。 |
完美转发 | 用于将参数传递给可调用对象,保留原始类型和值类别。 |
std::unique_ptr |
用于自动管理 FunctionWrapper 对象的生命周期,防止内存泄漏。 |
拷贝构造和赋值 | 通过 clone 函数实现深拷贝,确保每个 MyFunction 对象拥有独立的 FunctionWrapper 对象副本。 |
五、总结
std::function
的类型擦除机制是一种非常巧妙的设计,它允许我们在不知道具体类型的情况下,调用各种可调用对象。 这种机制在很多场景下都非常有用,比如事件处理、回调函数等等。
当然,类型擦除也有一些缺点,比如性能开销会略微增加,因为需要通过虚函数来调用可调用对象。 但是,在大多数情况下,这种性能开销是可以忽略不计的。
希望今天的讲座能够帮助大家更好地理解std::function
的类型擦除机制。 下次遇到类似的问题,你也可以自己动手实现一个类似的类型擦除类。
好了,今天的讲座就到这里,谢谢大家! 记住,编程的乐趣在于探索和创造!