【C++程序设计技巧】NVI(Non-Virtual Interface )

本文探讨了C++中NVI(Non-Virtual Interface)机制的设计理念与实践应用,通过对比不同实现方式,阐述了如何有效分离接口定义与实现,减少FBC(Fragile Base Class)问题的发生。

在C++的程序设计中有一些设计开发的典型技巧需要整理讨论,在此抛砖引玉,为自己做积累,请高人也多多指教。

1.简介

在标准C++库中我们可以看到这样的一个现象:

6个公有虚函数,并且都是std::exception::what()和其重载。

142个非公有虚函数。

这样设计的目的何在呢,为什么“多此一举”的把虚函数设置为非公有呢?

这就是NVI机制要求的:将虚函数声明为非公有,而将公有函数都声明为非虚——虚拟和公有选其一。

2.机制分析

程序员常常将基类中的虚函数公有化,来提供一个接口的定义(virtual的功劳)同时提供其实现(具体的一个实现)。

class Base{
 public:
     virtual void Foo(int){
         cout<< "Base's Foo!" << endl;
     };
};

问题就出在“同时”——一个定义了接口的形式,一个定义了默认的一个实现,显然这样的设计没有将接口定义和实现分来。在这个时候,我们可以使用模板方法模式的思想:

 
class Base{
 public:
     void Foo(){
         DoFoo1();
         DoFoo2();
     }//use DoFooX()
 private:
     virtual void DoFoo1(){
         cout << "Base's DoFoo1" <<endl;
     }
     virtual void DoFoo2(){
         cout << "Base's DoFoo2" <<endl;
     }
 };
 
class Derived: public Base{
 private:
     virtual void DoFoo1(){
         cout << "Derived's DoFoo1" << endl;
     };
};

函数Foo定义了接口的形式,而DoFooX()函数则实现了对Foo函数的行为定制,实现了接口定义和实现的分离,我们举一个例子来说明好处:如果我们希望在Foo中做一下CS(Critical Section)的加锁解锁控制:

若我们完成这样的接口与实现分离,那么我们的实现是在基类的接口处添加所需流程即可,子类不需要修改:

 
class Base{
 public:
     void Foo(){
         cout << "Locking" << endl;
         DoFoo1();
         DoFoo2();
         cout << "Unlocking" << endl;
     }//use DoFooX()
 private:
     virtual void DoFoo1(){
         cout << "Base's DoFoo1" <<endl;
     }
     virtual void DoFoo2(){
         cout << "Base's DoFoo2" <<endl;
     }
 
 };
 
 class Derived: public Base{
 private:
     virtual void DoFoo1(){
         cout << "Derived's DoFoo1" << endl;
     };
 };

若不实现接口与实现分离,则从基类到子类都需要修改:

 
class Base{
 public:
     virtual void Foo(){
         cout << "Locking" << endl;
         cout << "Base's Foo" << endl;
         cout << "Unlocking" << endl;
     }
 };
 
 class Derived: public Base{
 public:
     virtual void Foo(){
         cout << "Locking" << endl;
         cout << "Derived's Foo" << endl;
         cout << "Unlocking" << endl;
     };
 };

注意,当且仅当子类需要调用基类的虚函数时才将虚函数设置为protected(否则没有权限),并且NVI机制不适用于析构函数,对于析构函数,如果设为公有则应该设置为虚拟(在允许多态删除的基类中),否则设置为私有或者protected的非虚拟形式(不含多态删除的基类中)。

带来的风险:

首先是FBC问题(Fragile Base Class ),下边是一个例子:

class Set {
    std::set<int> s_;
  public:
    void add (int i) {
      s_.insert (i);
      add_impl (i); // Note virtual call.
    }
    void addAll (int * begin, int * end) {
      s_.insert (begin, end);   //  --------- (1)
      addAll_impl (begin, end); // Note virtual call.
    }
  private:
    virtual void add_impl (int i) = 0;
    virtual void addAll_impl (int * begin, int * end) = 0;
};
class CountingSet : public Set {
  private:
    int count_;
    virtual void add_impl (int i) {
      count_++;
    }
    virtual void addAll_impl (int * begin, int * end) {
      count_ += std::distance(begin,end);
    }
};

如果此时我们在父类中修改了addAll函数,改为将从begin到end的数字都调用一遍add函数,那么,子类的功能就紊乱了——子类计数就会多记录一倍(因为在子类中,add_impl每次都会计数一个,并且addAll_impl也会整体计数一次)。所以,为了防止出现FBC,一般一个公有非虚函数调用一个私有虚函数。

其次是性能上的考虑,毕竟多了一层函数调用。对此,参考文献2指出:“a word about efficiency: No, none is lost in practice because if the public function is a one-line passthrough declared inline, all compilers I know of will optimize it away entirely, leaving no overhead. (Indeed, some compilers will always make such a function inline and eliminate it, whether you personally really wanted it to or not, but that’s another story.)”

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值