C++类型擦除、依赖注入和外部多态学习笔记

MirrorYuChen
MirrorYuChen
发布于 2025-01-15 / 8 阅读
0
0

C++类型擦除、依赖注入和外部多态学习笔记

C++类型擦除、依赖注入和外部多态

1.类型擦除

​ 类型擦除(Type Erasure),顾名思义,就是将类型信息擦除掉,让部分代码无需知道使用对象的具体类型或具体实现,它主要用于代码解耦。常见实现方法可以分为三种:

1.1 基于 void*类型擦除

​ 熟悉C语言的同学对于 void*不会陌生,void*是C/C++语言中一个特殊的指针类型,它具有如下几个特点:

  • (1) 通用性void*可以指向任何类型的对象,但不能指向函数,它为不同类型数据间提供了一种通用指针操作方式;
  • (2) 类型转换:当需要将其它类型转换成 void*类型时,可以直接使用 void*进行强转,访问该数据时,再将其转换回原来类型;
  • (3) 大小void*指针大小与普通指针在相同平台上通常是一样的,都是用来存储一个内存地址。

​ 代码示例:

/*
 * @Description: main
 * @Author: chenjingyu
 * @Date: 2025-01-15 16:27:32
 * @FilePath: main.cc
 */
#include <stdio.h>
#include <string.h>

void *Memcpy(void *dst, const void *src, size_t n) {
  unsigned char *d = (unsigned char *)dst;
  const unsigned char *s = (const unsigned char *)src;
  for (size_t i = 0; i < n; ++i) {
    d[i] = s[i];
  }
  return dest;
}

int main(int argc, char* argv[]) {
  int src[] = {1, 2, 3, 4, 5};
  int dst[5];

  // 使用 my_memcpy 拷贝数组
  Memcpy(dst, src, sizeof(src));

  // 打印拷贝后的数组
  for (int i = 0; i < 5; ++i) {
    printf("%d ", dst[i]);
  }
  printf("\n");
}

​ 对于用户而言,Memcpy可以处理多种类型数据,且无需为每种类型编写单独处理函数。

1.2 基于类继承类型擦除

​ 继承按照意图可以分为两种类型:“实现继承”和“多态继承”。

  • (1) 实现继承:主要用于代码复用。通过继承,派生类可以自动共享基类的方法和属性,从而减少代码重复,提高开发效率。
/*
 * @Description: main
 * @Author: chenjingyu
 * @Date: 2025-01-15 16:27:32
 * @FilePath: main.cc
 */
#include <iostream>

class Writer {
public:
  void write() {
    std::cout << "Writer is writing." << std::endl;
  }
};

class Painter {
public:
  void paint() {
    std::cout << "Painter is painting." << std::endl;
  }
};

// 派生类
class Artist : public Writer, public Painter {
public:
  void createArt() {
    std::cout << "Artist is creating art." << std::endl;
  }
};

int main() {
  Artist artist;
  artist.write();     // 继承自 Writer 类
  artist.paint();     // 继承自 Painter 类
  artist.createArt(); // Artist 类自己的方法
  return 0;
}
  • (2) 多态继承:主要用于实现接口的一致性行为和动态绑定,从而提高代码灵活性和可扩展性。
/*
 * @Description: main
 * @Author: chenjingyu
 * @Date: 2025-01-15 16:27:32
 * @FilePath: main.cc
 */
#include <iostream>
#include <vector>

class Shape {
public:
  virtual ~Shape() = default;
  virtual void draw() const {
    std::cout << "Drawing a shape." << std::endl;
  }
};

class Circle : public Shape {
public:
  void draw() const override {
    std::cout << "Drawing a circle." << std::endl;
  }
};

class Rectangle : public Shape {
public:
  void draw() const override {
    std::cout << "Drawing a rectangle." << std::endl;
  }
};

class Triangle : public Shape {
public:
  void draw() const override {
    std::cout << "Drawing a triangle." << std::endl;
  }
};

void drawAllShapes(const std::vector<Shape*>& shapes) {
  for (const auto& shape : shapes) {
    shape->draw();
  }
}

int main() {
  std::vector<Shape*> shapes;
  shapes.push_back(new Circle());
  shapes.push_back(new Rectangle());
  shapes.push_back(new Triangle());

  drawAllShapes(shapes);

  for (auto& shape : shapes) {
    delete shape;
  }
  return 0;
}

​ 基于继承的类型擦除是一种在 C++ 中实现类型擦除的技术,通过使用基类指针或引用,可以隐藏具体派生类的类型信息,从而实现通用的接口。对于上面的 drawAllShapes接口中,通过定义一个基类 Shape,然后为每种具体消息类型定义派生类,最后,通过基类指针或引用统一处理所有形状。

1.3 基于模板的类型擦除

​ 通过模板和多态性,可以在运行时隐藏具体类型信息,从而实现通用接口。这种方法通常要定义一个抽象基类、一个模板派生类及一个封装类来管理对象。

/*
 * @Description: main
 * @Author: chenjingyu
 * @Date: 2025-01-15 16:27:32
 * @FilePath: main.cc
 */
#include <iostream>
#include <vector>
#include <memory>

// 抽象基类,定义了通用的操作
class TaskBase {
public:
  virtual ~TaskBase() = default;
  virtual void operator()() const = 0;
};

// 模板类,用于包装任意类型的函数对象
template<typename F>
class TaskModel : public TaskBase {
public:
  template <typename U>
  TaskModel(U &&f) : functor_(std::forward<U>(f)) {}
  void operator()() const override {
    functor_();
  }

private:
  F functor_;
};

// 封装类:对TaskModel进行封装
class Task {
public:
  template <typename F>
  Task(F &&f) {
    using ModelType = TaskModel<F>;
    ptr_ = std::make_unique<ModelType>(std::forward<F>(f));
  }
  void operator()() const {
    ptr_->operator()();
  }

private:
  std::unique_ptr<TaskBase> ptr_;
};

// 普通函数
void func1() {
  std::cout << "type erasure 1" << std::endl;
}

// 重载括号运算符的类
struct func2 {
  void operator()() const {
    std::cout << "type erasure 2" << std::endl;
  }
};


int main(int argc, char* argv[]) {
  // 普通函数
  Task t1{&func1};
  t1(); // 输出 "type erasure 1"

  // 重载括号运算符的类
  Task t2{func2{}};
  t2(); // 输出 "type erasure 2"

  // Lambda 表达式
  Task t3{[]() { std::cout << "type erasure 3" << std::endl; }};
  t3(); // 输出 "type erasure 3"
  return 0;
}

2.依赖注入

​ 依赖注入(Dependency Injection, DI)是一种涉及模式,用于减少代码间的耦合度,通过解耦合、提高可测试性、支持多种配置和实现,以及支持框架和库的集成,显著提高了代码的可维护性和可扩展性。它通过将一个类的依赖关系由外部提供,而不是类内直接创建,从而实现依赖关系的解耦。

/*
 * @Description: main
 * @Author: chenjingyu
 * @Date: 2025-01-15 16:27:32
 * @FilePath: main.cc
 */
#include <iostream>
#include <string>
#include <memory>

// 日志记录接口
class Logger {
public:
  virtual ~Logger() = default;
  virtual void log(const std::string& message) = 0;
};

// 控制台日志记录器
class ConsoleLogger : public Logger {
public:
  void log(const std::string& message) override {
    std::cout << "Console: " << message << std::endl;
  }
};

// 文件日志记录器
class FileLogger : public Logger {
public:
  void log(const std::string& message) override {
    std::cout << "File: " << message << std::endl;
  }
};

// 用于注入具体的日志记录器
class LoggerInjector {
public:
  explicit LoggerInjector(std::unique_ptr<Logger> logger) : logger_(std::move(logger)) {}
  void log(const std::string& message) {
    logger_->log(message);
  }

private:
  std::unique_ptr<Logger> logger_;
};

int main(int argc, char* argv[]) {
  std::unique_ptr<Logger> console_logger(new ConsoleLogger);
  std::unique_ptr<Logger> file_logger(new FileLogger);

  {
    LoggerInjector injector(std::move(console_logger));
    injector.log("Hello, world!");
  }

  {
    LoggerInjector injector(std::move(file_logger));
    injector.log("Hello, world!");
  }
  return 0;
}

​ 这个是比较经典的日志单例模式实现过程中,很难实现真正的单例,所以一个比较好的办法就是采用依赖注入方式实现一个全局唯一的日志记录器,进而可以在不同模块中灵活使用它。

3.外部多态(External Polymorphism)

​ 外部多态是一种设计模式,主要目的是在不修改现有类的情况下,为这些类添加多态行为。这种模式特别适用于那些不能或不应该被修改的类,例如,第三方库中的类。外部多态通过外部构建一个平行的类继承结果实现多态,从而避免对现有类的直接修改。

/*
* @Description: main
* @Author: chenjingyu
* @Date: 2025-01-15 16:21:41
* @FilePath: main.cc
*/
#include <iostream>
#include <vector>

class A {
public:
  explicit A(int a, int b) : a_(a), b_(b) {}
  ~A() = default;
  void run() const {
    std::cout << "A::run() with a_ = " << a_ << " and b_ = " << b_ << std::endl;
  }

private:
  int a_;
  int b_;
};

class B {
public:
  explicit B(int b) : b_(b) {}
  ~B() = default;
  void run() const {
    std::cout << "B::run() with b_ = " << b_ << std::endl;
  }

private:
  int b_;
};

/// Interface
struct Interface {
  virtual void run() = 0;
  virtual ~Interface()  = default;
};

/// Implement
template <typename ConcreteType>
class Implement : public Interface {
public:
  template<class... Args>
  explicit Implement(Args&&... args) : concrete_(std::forward<Args>(args)...) {}
  void run() override {
    std::cout << "Implement<" << typeid(ConcreteType).name() << ">::run()" << std::endl;
    concrete_.run();
  }
  ~Implement() override = default;

private:
  ConcreteType concrete_;
};

template <typename ConcreteType, class... Args>
std::unique_ptr<Interface> createConcrete(Args&&... args) {
  return std::make_unique<Implement<ConcreteType>>(std::forward<Args>(args)...);
}

int main(int argc, char *argv[]) {
  std::vector<std::unique_ptr<Interface>> vec;
  vec.push_back(createConcrete<A>(1, 2));
  vec.push_back(createConcrete<B>(2));
  for (auto &ptr : vec) {
    ptr->run();
  }
  return 0;
}

​ 注意,这里 AB都是假定外部库提供的类型。于此同时,这里其实要求所有类都是具有一个方法 run,那么真实使用过程中,一些类没有这些方法怎么办?答案就是可以用函数重载的方式给每个类添加一个公有方法,例如,添加一个 jump方法:

/*
* @Description: main
* @Author: chenjingyu
* @Date: 2025-01-15 16:21:41
* @FilePath: main.cc
*/
#include <iostream>
#include <vector>

class A {
public:
  explicit A(int a, int b) : a_(a), b_(b) {}
  ~A() = default;
  void run() const {
    std::cout << "A::run() with a_ = " << a_ << " and b_ = " << b_ << std::endl;
  }

private:
  int a_;
  int b_;
};

class B {
public:
  explicit B(int b) : b_(b) {}
  ~B() = default;
  void run() const {
    std::cout << "B::run() with b_ = " << b_ << std::endl;
  }

private:
  int b_;
};

namespace detail {
void jump(const A &a) {
  std::cout << "jumping from A" << std::endl;
  a.run();
}

void jump(const B &b) {
  std::cout << "jumping from B" << std::endl;
  b.run();
}
}  // namespace detail

/// Interface
struct Interface {
  virtual void run() = 0;
  virtual void jump() = 0;
  virtual ~Interface()  = default;
};

/// Implement
template <typename ConcreteType>
class Implement : public Interface {
public:
  template<class... Args>
  explicit Implement(Args&&... args) : concrete_(std::forward<Args>(args)...) {}
  void run() override {
    std::cout << "Implement<" << typeid(ConcreteType).name() << ">::run()" << std::endl;
    concrete_.run();
  }

  void jump() override {
    detail::jump(concrete_);
  }
  ~Implement() override = default;

private:
  ConcreteType concrete_;
};

template <typename ConcreteType, class... Args>
std::unique_ptr<Interface> createConcrete(Args&&... args) {
  return std::make_unique<Implement<ConcreteType>>(std::forward<Args>(args)...);
}

int main(int argc, char *argv[]) {
  std::vector<std::unique_ptr<Interface>> vec;
  vec.push_back(createConcrete<A>(1, 2));
  vec.push_back(createConcrete<B>(2));
  for (auto& ptr : vec) {
    ptr->run();
    ptr->jump();
  }
  return 0;
}

4.参考资料


评论