C++ 自定义 `std::function`:理解其类型擦除机制

好的,各位观众老爷,欢迎来到今天的“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)的类型擦除机制:

  1. 基类FunctionWrapper 定义了统一的接口invoke,用于调用可调用对象。它是一个抽象类,不能直接实例化。
  2. 派生类FunctionCaller 模板类,用于包装具体的可调用对象。它继承自FunctionWrapper,并实现了invoke函数,实际调用存储的可调用对象。
  3. MyFunction类: 负责管理FunctionWrapper对象。 它使用std::unique_ptr来管理FunctionWrapper对象的生命周期。它还重载了函数调用运算符,用于实际调用存储的可调用对象。
  4. 虚函数: FunctionWrapper中的invoke函数是虚函数,这使得我们可以通过基类指针来调用派生类的invoke函数,从而实现多态。
  5. 模板: FunctionCaller是一个模板类,这使得我们可以包装任意类型的可调用对象。
  6. 完美转发: 使用完美转发可以将参数传递给可调用对象,而无需关心参数的类型。
  7. 拷贝构造和拷贝赋值: 通过 clone 方法来复制 FunctionWrapper 对象,避免多个 MyFunction 对象共享同一个 FunctionWrapper 对象。

用表格来总结一下:

组件 功能
FunctionWrapper 定义统一的调用接口 ( invoke ),是抽象基类。包含一个纯虚函数 clone 用于创建对象副本。提供虚析构函数。
FunctionCaller 模板类,用于包装具体的可调用对象。 实现 invoke 函数,实际调用存储的可调用对象。 实现 clone 函数,创建当前对象的副本。
MyFunction 管理 FunctionWrapper 对象。 提供构造函数、拷贝构造函数、拷贝赋值运算符和析构函数。 重载函数调用运算符,用于调用存储的可调用对象。 使用 std::unique_ptr 管理 FunctionWrapper 对象的生命周期。 提供显式类型转换运算符,用于判断对象是否为空。
虚函数 FunctionWrapper 中的 invokeclone 是虚函数,允许通过基类指针调用派生类的实现,实现多态。
模板 FunctionCaller 是模板类,允许包装任意类型的可调用对象。
完美转发 用于将参数传递给可调用对象,保留原始类型和值类别。
std::unique_ptr 用于自动管理 FunctionWrapper 对象的生命周期,防止内存泄漏。
拷贝构造和赋值 通过 clone 函数实现深拷贝,确保每个 MyFunction 对象拥有独立的 FunctionWrapper 对象副本。

五、总结

std::function的类型擦除机制是一种非常巧妙的设计,它允许我们在不知道具体类型的情况下,调用各种可调用对象。 这种机制在很多场景下都非常有用,比如事件处理、回调函数等等。

当然,类型擦除也有一些缺点,比如性能开销会略微增加,因为需要通过虚函数来调用可调用对象。 但是,在大多数情况下,这种性能开销是可以忽略不计的。

希望今天的讲座能够帮助大家更好地理解std::function的类型擦除机制。 下次遇到类似的问题,你也可以自己动手实现一个类似的类型擦除类。

好了,今天的讲座就到这里,谢谢大家! 记住,编程的乐趣在于探索和创造!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注