[读书笔记]《Hands on Design Patterns with C++》—— CRTP

多态是指使用父类可以访问基类的内容,并且基类对子类的内容毫不知情,因为父类已经写了或者被编译了,但是基类可能还没有没编写出来。但是有一个 idiom, Curiously Recurring Template Pattern (CRTP) 则是完全颠覆了这一情况。

要了解学习 CRTP,我们首先需要讨论的是, virtual function 有什么问题? 虚函数最大的是性能负担,因为需要一个额外的虚函数指针,包括调用,间接跳转。

template <typename D> class B {
    ...
};

class D : public B<D> {
   ...
}

首先第一个区别就是基类现在是一个 模板类。子类依旧是继承自基类,不过是继承基类的一个特例化,这个特例化的类型是子类它自己。上面 B 由 D 实例化,然后 D 继承 B,然后 B 又由 D 实例,这样不断的递归下去,因此而得名。

考虑到现在基类有了子类的编译期信息,因此,类似于虚函数,我们就可以在编译期调用到正确的函数了。

template <typename D>
class B {
public:
    B() : i_(0) {}
    void f(int i) { static_cast<D *>(this)->f(i); }
    int get() const { return i_; }

protected:
    int i_;
};

class D : public B<D> {
public:
    void f(int i) { i_ += i; }
};

上面是直接调用基类中的成员函数,而没有虚函数的间接跳转和性能负担。

CRTP 的一个主要限制是基类 B 的大小不能依赖于模板参数 D。例如下面的代码就会编译失败。

template <typename D>
class B {
 typedef typename D::T T;
  T* p_;
};

class D : public B<D> {
public:
  typedef int T;
};

上面编译失败的原因不是 B 的定义,问题是出在我们我们想要使用:

class D : public B …

这时,因为 B 应该是已知的类型,但是类型 D 还没有被定义,但是 D 的声明又需要知道确切的 B 类型。相互依赖就发生了错误。

Compile-time polymorphism

首先多态就是允许子类 override 基类的虚函数,然后通过基类指向子类对象,实际调用时,来访问到子类的实现。前面可以看到,CRTP 也可以允许子类自定义基类的行为,并根据具体的实例化调用相关子类的实现。

D* d = ...;
d->f(5);  // 调用 D::f()
B<D>* b = ...;
b->f(5);  // 调用 D::f()

我们可以直接获得一个 子类 D 的对象,然后直接调用 D::f(),或者通过基类的指针,通过基类指针注意不是 B*,而是 B*,这里基类最大的不同是,子类 D 需要在编译期就明确知道。

当我们想要用上面的 CRTP 结构,我们应该怎样来写一个调用它的函数呢?答案是模板函数。

template <typename D>
void apply(B<D>* b, int& i){
    b->f(++i);
}

The compile-time pure virtual function

在这种情况下**,**纯虚函数又对应成什么样子呢?纯虚函数的性质就是每一个子类必须都要实现一遍。 一个类有一个纯虚函数,那它就是抽象基类,它可以被继承,但是不能被实例化。还是下面的例子:

template <typename D>
class B{
public:
    ...
    void f(int i) { static_cast<D *>(this)->f(i); }

};

class D : public B<D>
{
public:
    ...  // 没有实现 f()
};

子类D 并没有实现 f(),如果调用 D::f() 不会报错,它会继承一个 B::f(),然后在 B::f() 中又会调用 D::f(),就这样会无限递归调用 B::f() 下去。

上面问题主要是因为我们把接口 Interface 和实现 implementation 混在了一起(用了相同的名字)。接口就是 public 函数 f(),这相当于基类表示,我所有的子类都的有这个接口。然后子类则对这个接口提供实际的实现。现在一个可行的方式就是将接口和实现使用不同的名字。

template <typename D>
class B{
public:
    ...
    void f(int i) { static_cast<D *>(this)->f_impl(i); }

};

class D : public B<D>
{
public:
    void f_impl(int i) {i_ += i;}
};

这样,如果继承的子类 D 没有实现 f_impl,代码就会编译错误。这样我们就达到了类似纯虚函数的效果。这里虚函数的角色实际是 f_impl() 而不再是 f(),如果要像正常的虚函数那样,在基类 B 中就需要提供一个默认的 f_impl 的函数实现。


Destructors and polymorphic deletion

虚函数中还有一个比较重要的点是基类析构函数必须是虚的,否则可能会引起内存泄漏,对于 CRTP 我们同样会遇到这样的问题。

B<D>* b = new D;
...
delete b;

上面的代码,直接删除的是基类指针 B*, 而 D 类的析构函数不会被调用。你可能想使用在模板基类 B 中调用 D 的析构函数来进一步达到析构 D 的手段。

template <typename D> class B {
  public:
   ~B() {static_cast<D*>(this)->~D(); }
};

上面的做法是行不通的,首先因为基类自身的析构函数中,实际的对象已经不再是任何子类类型,这时调用任何子类的成员函数都是有未定义的行为。其次就是即使上面的行得通了,子类的析构函数开始运行了,它又会调用父类的析构函数,这样会引起无限循环。

要解决上面的问题有以下两个方式,一个是定义一个模板函数来完成删除子类的过程。删除对象就要记得一直使用 destroy 函数方式,而不是直接调用 delete 操作符。

template <typename D> void destroy(B<D>* b) {delete static_cast<D*>(b);}

另一种方法就是模板基类的析构函数声明成虚函数。不过这就会引入额外的虚函数的负担。

CRTP and access control

当实现 CRTP 时,还需要考虑的一个点就是访问权限。在基类中调用的任何子类成员函数必须得有访问权限。要么就是这个接口是 public 的,大家都可以访问,要么就是调用者有特殊的访问权限。
虚函数因为每一个有虚函数的对象都有一个虚函数表,所以可以直接访问虚函数表中的所有函数。而 CRTP 中基类对子类的特殊访问权限只有通过 friend 关键字来实现。

template <typename D> class B {
public:
    void f(int i) { static_cast<D*>(this)->f_impl(i); }
private:
    B(): i_(0) {}
    void f_impl(int i) {}
    int i_;
    friend D;
};

class D : public B<D>
{
private:
    void f_impl(int i) { i_ += i; }
    friend class B<D>; // 友元类
};

注意在模板中友元的声明方式是 friend D,而不是 friend class D。将模板基类 B 的构造函数设置为私有,这样就不能通过 new 直接创建一个模板基类 B 的对象,而把传入的模板参数设置成 friend,使用子类就可以访问基类 B 的构造函数。因此现在唯一可以构造类 B 示例的方式就是通过特定的派生类 D,作为模板参数来创建。

Expanding the interface

现在我们通过模板基类来拓展派生类的接口。下面就是对于任何已经提供了 operator==() 的类,我们想自动拓展它的 operator≠()。


template <typename T>
struct not_equal{
  bool operator != (const T& rhs) {
        return static_cast<T*>(this)->operator(rhs);
  }
};

class D : public not_equal<D> {
 int i_;
 public:
  D(int i) : i_(i) { }
  bool operator==(const D& rhs) {
      return this->i_ == rhs.i_;
  }
};

当然这里也可以写成非成员函数形式,但是依旧要访问类的私有变量,所以得声明成友元关系。

template <typename T>
struct not_equal {
  friend bool operator!= (const T& lhs, cnost T& rhs) {
      return !(lhs == rhs);
  }
};

class D : public not_equal<D> {
 int i_;
 public:
 D(int i) : i_(i) { }
 friend bool operator==( const D& lhs, const D& rhs) {
     return lhs.i_ == rhs.i_;
 }
};

一个相似但是稍微复杂的例子就是对象的注册。我们并不想每一个子类都重复写一套注册机制,所以要将其移动到基类中,不过现在有子类 C 和 D, 它们都继承自 B,在 B 中,无法区分子类的具体类型,对它们计数将会把 C 和 D 计在一起。我们需要的是每一个子类都有一个计数器,想要这样就需要基类在编译期就知道子类的具体类型。 CRTP 可以带来我们想要的效果。

template <typename D> 
class registry {
public:
  static size_t count;
  static D* head;
  D* pre;
  D* next;
protected:
  registry() {
      ++count;
      pre = nullptr;
      next = head;
      head = static_cast<D*>(this);
      if(next) next->pre = head;
  }
  
  registry(const registry&) {
      ++count;
      pre = nullptr;
      next = head;
      head = static_cast<D*>(this);
      if(next) next->pre = head;
  }

  ~registry() {
      --count;
      if(pre) pre->next = next;
      if(next) next->pre = pre;
      if(head == this) head = next;
  }
};
template <typename D> size_t registry<D>::count(0);
template <typename D> D* registry<D>::head(nulllptr);

这里声明了基类的构造函数,析构函数为 protected,是不想除了子类外,还有其他父类对象被创建,这里主要还要实现拷贝构造,因为默认的是不会处理 count 计数的。 对每一个子类 D,基类是 registry, 它是一个单独的类型,有它自己的静态变量。现在所有需要维持一个运行时动态注册的类型,只需要继承 registry 就可以了。

class C : public registry<C> {
  int i_;
  public:
   C(int i) : i_(i){ }
}

另一个比较常见的 delegate 给子类实现的情况是 访问者模式。 访问者是指调用来处理一堆数据的对象,会逐个访问这些数据,并依次执行函数。 假设现在有一个访问函数来访问不同的动物种类。

struct Animal {
  public:
    enum Type{ CAT, DOG, RAT};
    Animal(Type t, const char* n) : type(t), name(n) { }
    const Type type;
    const char* const name;
};

template <typename D> 
class GenericVisitor {
  public:
    template<typename it> void visit(it from, it to) {
        for(it i=from; i!=to; ++i) {
            this->visit(*i);
        }
    }
  
  private:
    D& derived() {return *static_cast<D*>(this);}
    void visit(const Animal& animal) {
        switch (animal.type) {
            case Animal::CAT:
              derived().visit_cat(animal); break;
            case Animal::DOG:
              derived().visit_dog(animal); break;
            case Animal::RAT:
              derived().visit_rat(animal); break;
        }
    }
    
    void visit_cat (const Animal& animal) {
        std::cout << "Feed the cat " << animal.name << std::endl;
    }
    void visit_dog (const Animal& animal) {
        std::cout << "Wash the dog " << animal.name << std::endl;
    }
    void visit_rat (const Animal& animal) {
        std::cout << "Eeek " << animal.name << std::endl;
    }

    friend D;
    GenericVisitor() { }
};

所以使用的话,下面是默认的访问行为:

class DefaultVistor : public GenericVisitor<DefaultVistor> {
};

// 使用
std::vector<Animal> animals {{Animal::CAT, "Fluffy"},
                                 {Animal::DOG, "Fido"},
                                 {Animal::RAT, "jerry"}};
    
DefaultVistor().visit(animals.begin(), animals.end());

如果不想要上面的默认行为,我们可以自己 override 上面的访问函数。

class TrainerVisitor : public GenericVisitor<TrainerVisitor> {
    friend class GenericVisitor<TrainerVisitor>;
    void visit_dog(const Animal& animal) {
        std::cout << "Train the dog " << animal.name << std::endl;
    }
};

class FelineVisitor : public GenericVisitor<FelineVisitor> {
    friend class GenericVisitor<FelineVisitor>;
    void visit_cat (const Animal& animal) {
        std::cout << "Hiss the cat " << animal.name << std::endl;
    }
    void visit_dog (const Animal& animal) {
        std::cout << "Hiss the dog " << animal.name << std::endl;
    }
    void visit_rat (const Animal& animal) {
        std::cout << "Eat the rat " << animal.name << std::endl;
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值