美文网首页
19 桥接静态多态与动态多态

19 桥接静态多态与动态多态

作者: 奇点创客 | 来源:发表于2021-06-01 09:04 被阅读0次

函数对象指针std::function

  • 函数对象用于给模板提供一些可定制行为
#include <iostream>
#include <vector>

template<typename F>
void forUpTo(int n, F f)
{
  for (int i = 0; i < n; ++i) f(i);
}

void print(int i)
{
  std::cout << i << ' ';
}

int main()
{
  std::vector<int> v;
  forUpTo(5, [&v] (int i) { v.push_back(i); });
  forUpTo(5, print); // prints 0 1 2 3 4
}
  • forUpTo()可用于任何函数对象,每次使用forUpTo()都将产生一个不同的实例化,这个模板很小,但如果很大则实例化的代码也会很大。一个限制代码增长的方法是将其改为无需实例化的非模板
void forUpTo(int n, void (*f)(int))
{
  for (int i = 0; i < n; ++i) f(i);
}
  • 但这样就不允许接受lambda,因为lambda不能转为函数指针
forUpTo(5, [&v] (int i) { v.push_back(i); }); // 错误:lambda不能转为void(*)(int)
#include <functional>

void forUpTo(int n, std::function<void(int)> f)
{
  for (int i = 0; i < n; ++i) f(i);
}
  • 这个实现提供了一些静态多态方面的特点,它能处理函数对象,但本身只是单个实现的非模板函数。它使用类型擦除(type erasure)的技术做到这点,即在编译期去掉不需要关心的原有类型(与实参推断相反),类型擦除桥接了静态多态与动态多态的沟壑

实现std::function

  • std::function是一个广义的函数指针形式,不同于函数指针的是,它能存储一个lambda或其他任何带有合适的operator()的函数对象。下面实现一个能替代std::function的类模板
#include <iostream>
#include <vector>
#include <type_traits>

template<typename R, typename... Args>
class B { // 桥接口:负责函数对象的所有权和操作
 public: // 实现为抽象基类,作为类模板A动态多态的基础
  virtual ~B() {}  
  virtual B* clone() const = 0;
  virtual R invoke(Args... args) const = 0;
};

template<typename F, typename R, typename... Args>
class X : public B<R, Args...> { // 抽象基类的实现
 private:
  F f; // 参数化存储的函数对象类型,以实现类型擦除
 public:
  template<typename T>
  X(T&& f) : f(std::forward<T>(f)) {}

  virtual X* clone() const override
  {
    return new X(f);
  }

  virtual R invoke(Args... args) const override
  {
    return f(std::forward<Args>(args)...);
  }
};

// 原始模板
template<typename Signature>
class A;

// 偏特化
template<typename R, typename... Args>
class A<R(Args...)> {
 private:
  B<R, Args...>* bridge; // 该指针负责管理函数对象
 public:
  A() : bridge(nullptr) {}

  A(const A& other) : bridge(nullptr)
  {
    if (other.bridge)
    {
      bridge = other.bridge->clone();
    }
  }

  A(A& other) : A(static_cast<const A&>(other)) {}

  A(A&& other) noexcept : bridge(other.bridge)
  {
    other.bridge = nullptr;
  }

  template<typename F>
  A(F&& f) : bridge(nullptr) // 从任意函数对象构造
  {
    using Functor = std::decay_t<F>;
    using Bridge = X<Functor, R, Args...>; // X的实例化存储一个函数对象副本
    bridge = new Bridge(std::forward<F>(f)); // 派生类到基类的转换,F丢失,类型擦除
  }

  A& operator=(const A& other)
  {
    A tmp(other);
    swap(*this, tmp);
    return *this;
  }

  A& operator=(A&& other) noexcept
  {
    delete bridge;
    bridge = other.bridge;
    other.bridge = nullptr;
    return *this;
  }

  template<typename F>
  A& operator=(F&& f)
  {
    A tmp(std::forward<F>(f));
    swap(*this, tmp);
    return *this;
  }

  ~A() { delete bridge; }

  friend void swap(A& fp1, A& fp2) noexcept
  {
    std::swap(fp1.bridge, fp2.bridge);
  }

  explicit operator bool() const
  {
    return bridge == nullptr;
  }

  R operator()(Args... args) const
  {
    return bridge->invoke(std::forward<Args>(args)...);
  }
};

void forUpTo(int n, A<void(int)> f)
{
  for (int i = 0; i < n; ++i) f(i);
}

void print(int i)
{
  std::cout << i << ' ';
}

int main()
{
  std::vector<int> v;
  forUpTo(5, [&v] (int i) { v.push_back(i); });
  forUpTo(5, print);
}
  • 上述A模板还不支持函数指针提供的一个操作:测试是否两个A对象将调用相同的函数。这需要桥接口B提供一个equals操作
virtual bool equals(const B* fb) const = 0;
  • 接着在X中实现
virtual bool equals(const B<R, Args...>* fb) const override
{
  if (auto specFb = dynamic_cast<const X*> (fb))
  {
    return f == specFb->f; // 要求f有operator==
  }
  return false;
}
  • 最终为A实现operator==
friend bool operator==(const A& f1, const A& f2) {
  if (!f1 || !f2)
  {
    return !f1 && !f2;
  }
  return f1.bridge->equals(f2.bridge); // equals要求operator==
}

friend bool operator!=(const A& f1, const A& f2)
{
  return !(f1 == f2);
}
  • 这个实现还有一个问题:如果A用没有operator==的函数对象赋值或初始化,编译将报错,即使A的operator==还没被使用。这个问题来源于类型擦除:一旦A被赋值或初始化,就丢失了函数对象的类型(派生类到基类的转换),这就要求在实例化前得知类型信息,这个信息包括对一个函数对象的operator==的调用。为此可以用SFINAE-based traits检查operator==是否可用
template<typename T>
class IsEqualityComparable {
 private:
  static void* conv(bool);
  template<typename U>
  static std::true_type test(
    decltype(conv(std::declval<const U&>() == std::declval<const U&>())),
    decltype(conv(!(std::declval<const U&>() == std::declval<const U&>())))
  );

  template<typename U>
  static std::false_type test(...);
 public:
  static constexpr bool value = decltype(test<T>(nullptr, nullptr))::value;
};

// 构造一个TryEquals在调用类型没有合适的==时抛出异常
template<typename T, bool EqComparable = IsEqualityComparable<T>::value>
struct TryEquals {
  static bool equals(const T& x1, const T& x2)
  {
    return x1 == x2;
  }
};

class NotEqualityComparable : public std::exception {};

template<typename T>
struct TryEquals<T, false> {
  static bool equals(const T& x1, const T& x2)
  {
    throw NotEqualityComparable();
  }
};
  • 在X中实现equals时,使用TryEquals替代operator==即可达到同样的目的
virtual bool equals(const B<R, Args...>* fb) const override
{
  if (auto specFb = dynamic_cast<const X*> (fb))
  {
    return TryEquals<F>::equals(f, specFb->f);
  }
  return false;
}

性能考虑

  • 类型擦除同时提供了部分静态多态和动态多态的优点。使用类型擦除的泛型代码的性能更接近于动态多态,因为两者都通过虚函数使用动态分派,因此可能丢失一些静态多态的传统优点,如编译器内联调用的能力。虽然这种性能损失能否感知依赖于应用程序,但通常很容易判断。考虑相对虚函数调用开销需要执行多少工作:如果两者接近,类型擦除可能比静态多态慢得多,反之如果函数调用执行大量工作,如查询数据库、排序容器或更新用户接口,类型擦除的开销就算不上多大

相关文章

  • 19 桥接静态多态与动态多态

    函数对象[https://en.cppreference.com/w/cpp/named_req/Function...

  • 【C++ Templates(20)】桥接静态多态与动态多态

    函数对象、指针与std::function<> 函数对象用于给模板提供一些可定制行为,比如下面的函数模板枚举从0到...

  • 多态(1)静态多态与动态多态

    多态(1)静态多态与动态多态 什么是多态 从字面上理解就是多种形态的意思。而多态一词最初源自希腊语,其含义便是“多...

  • 动态多态、静态多态

    C++支持多种形式的多态,从表现的形式来看,有虚函数、模板、重载等,从绑定时间来看,可以分成静态多态和动态多态,也...

  • 二继承与多态——第四节、多态

    文章目录1、 从吃烤山药重新认识多态2、 多态前提条件【重点】3、 多态的体现4、 多态动态绑定与静态绑定4、1....

  • 多态和虚函数

    多态可以分为静态多态和动态多态 静态多态:函数重载,泛型编程,编译器在编译期间内完成的,编译器根据函数实参的类型可...

  • c++ ——为什么要使用多态?

    多态可分为静态多态和动态多态 静态多态就是在系统编译期间就可以确定程序执行到这里将要执行哪个函数,比如函数的重载。...

  • 18 类和对象--多态

    我们知道类的3大特性是封装,继承,多态。前2个将差不多了,多态其实也不难。 多态的基本概念 多态分为静态多态和动态...

  • 多态与重载的区别

    重载可认为是静态的多态,静态联编,发生在编译阶段; 多态是动态的,动态联编,发生在运行阶段; 静态的比动态的效率高...

  • 查漏补缺

    C++虚函数: 多态: 静态多态(重载)、动态多态(虚函数) 虚函数 虚函数表:编译器为每个类创建了一个虚函数表...

网友评论

      本文标题:19 桥接静态多态与动态多态

      本文链接:https://www.haomeiwen.com/subject/rnnvjltx.html