C++中并没有与Java中等价的接口的概念,但通常来说我们可以以包含纯虚函数的方式来定义一个接口。但是,这里想要说明一种基于模板特例化构建更加灵活的、但不支持多态的接口。

定义接口的目的

说方法之前,先给出动机。

Java中的interface算是有一点了解,就是定义了一系列操作,如果runable的接口就是定义了可飞行接口,一个类如果需要飞行,那么只需要把runable作为接口,并实现其中的方法就OK。个人想来,用接口有两个好处(目的),一个是定义了操作规范,即必须包含满足接口中定义的ret-type interface-xxx(param-xxx)这样的操作;第二点就是通过接口,可以实现泛型(Java中是这么叫的吧…),使得代码更加灵活(如设计模式中的“策略模式”可能就需要这样的支持)。

使用C++实现等价接口

在C++中,没有关键字来定义接口,但是也很容易就实现。我们只需要定义一个完全由纯虚函数构成的基类,就可以算是接口了。这样子类凡是没有实现接口的就不能实例化;多态特性也使得其所有的子类都可以由此基类的指针或引用来指向。所以,C++中应该是完全可以实现与Java等价的接口。

不过由于C++并没有从语法上定义接口,因此在实现上述基类时,我们还可以定义一些非纯虚函数,用来实现默认的操作。这大概又算是Java中的适配器?

下面是sample:

class CplusInterface
{
    virtual void fly() = 0;
    virtual void fly_down() { ... };
    virtual ~CplusInterface(); // A virtual destructor is needed.
};

以上的讨论是常见的C++用来实现接口的方法,其实现的效果与Java中等价,因此也算是广泛应用吧。

PS, 由于对Java已经算是遗忘了,所以上述论述很可能不准确甚至是错误的。

不以多态为目的的接口

上述完整意义的接口有时会限制我们可以做的事情。最直观的就是,如果A和B都需要一系列操作,但是这些操作又各自包含各自特有的性质,那么定义一个基于纯虚函数的基类就不能解决问题了。

这个需求是在项目中遇到的:

在做一个序列标注的任务,用的人家的NN框架。我想把NN的操作都给封装起来,并且提供一套统一的接口。这样在其他代码中我只需要使用这些统一的接口就好了,这样在一定程度上解耦了NN框架和自己的预处理逻辑。然而问题在于,NN框架的有些接口没法完整的封装起来,就是说,在一些操作中,必须有一些NN框架中的变量暴露出来。比如这个NN::Tensor* forward(const NN::expr &)接口,由于C++11中不能直接将模板和虚函数同时使用,我就没法在基类中定义这个接口!总不能把这个类型写死吧,那样还有什么意义?

想过一种尝试,就是可以在基类中定义一个wrapper类,然后定义接口virtual wrapperBaseTensor* forward(const wrapperBaseExpr&),然后在派生类中我们再定义wrapperDerived : wrapper类,定义接口`virtual wrapperDerivedTensor* forward(const wrapperBaseExpr&) override,理论上而言,这个是没有问题的!(注意,返回类型满足继承关系的就可以override,但是参数类型必须完全一致。)

以下是基于以上的思想实现的Sample代码:

#include <iostream>
using namespace std ;

struct Base
{
    struct ParamT
    { 
        int i = 12; 
    };
    struct RetT
    {
        int i = 10;
    };
    virtual RetT* print(const ParamT&) = 0;
    virtual ~Base(){};
};

struct Derived : Base
{
    struct DParamT : Base::ParamT
    {
        int k = 56;
    };
    struct DRetT : Base::RetT
    {
        int k = 20;
    };
    virtual DRetT* print(const Base::ParamT& dp) override
    { 
        const DParamT *dp_ptr = static_cast<const DParamT*>(&dp); // static_cast!
        const DParamT& dp_ref = static_cast<const DParamT&>(dp);
        cout << dp_ptr->k << " " << dp_ptr->i << endl;
        cout << dp_ref.k << " " << dp_ref.i << endl; 
        return new DRetT();
    }
};


int main(int argc , char *argv[])
{
    Derived d;
    Derived::DParamT dp;
    Base &b = d;
    Base::RetT *rp = b.print(dp);
    Derived::DRetT* drp = static_cast<Derived::DRetT*>(rp);
    cout << drp->i << " " << drp->k << endl;
    delete drp;
    return 0;
}

这个在G++4.8下没有问题。所以这么做也是可以的!所以通过构建一个Wrapper的方式,我们还是可以在之前的方法下实现这个需求。

只是,加一个Wrapper就得涉及到封包与解包,以及强制类型转换,也许会带来一定量的性能损失(也许… 因为没有真正做过啊!!)。

由于在此需求下其实我们并不关注多态,我们只是想定义这样一套操作规范!这个时候我们就可以使用基于模板特例化的方法。通过这样的方法来做:

template <size_t BackEndTag, typename TensorTmpT, typename ExprTmpT>
class NNInterface
{
public:
    using TensorT = TensorTmpT;
    using ExprT = ExprTmpT;
    TensorT* forward(const ExprT&);
};

template<>
class NNInterface<XXTag, XX::Tensor, XX::Expr>
{
public:
    using TensorT = XX::Tensor;
    using ExprT = XX::Expr;
    TensorT* forward(const ExprT&){ ... }
}

using XXNNInterface = NNInterface<XXTag, XX::Tensor, XX::Expr>;

通过以上方式我们就完成了一个基于模板的接口定义,并且通过特例化完成了一个实例的定义。这种方法的好处就是非常灵活:因为其实特例化的类与非特例化的模板类可以毫无关系(除了传入模板参数外),其实特例化的模板类中的内容完全可以随意写,也不会像基于虚基类继承的接口,如果没有实现接口就不能实例化——基于模板特例化的接口,完全可以认为是独立的。从一个角度上来说,接口模板完全就是一个注释,你怎么写都对代码运行没有影响。然而如果你想真正地把这种方法应用于实际,那么还是应该完全自觉按照接口来写,这样才真正写出的具有同一套接口的代码。

最后总结一下,基于模板特例化的方法,本质上已经从语法上脱离了接口必须实现的要求,但是从实际任务的角度,我们需要利用这种方法提供的灵活性,完成一种良好的代码封装。我们可以参考各种STL的库,比如每种容器都是有size_type, difference_type, begin()等通用接口的。它们并没有像Java那样有共同的基类,也甚至没有在实际代码实现中写这样写了一个所谓的基于模板特例化的接口,但是,它们一定是按照了某个规范来做的。这就是STL美妙额地方。而这里提到的方法,就是显示地把这种接口给写了出来,其作用——主要是提醒接口的实现者,不要忘了要实现这个功能…