純粋な虚関数のデフォルトの実装


新しい同僚のコード審査を手伝うとき、コードにはベースクラスBと派生クラスD 1があり、現在は派生クラスD 2が追加され、関数f 2()がある.経験不足のため,新しい同僚はD 1にも類似の関数f 1()があることに気づかなかった.これにより,類似のコードが2つの場所に現れ,コード冗長性は将来のメンテナンス作業に異常な困難をもたらす.f()は実際には汎用的な動作であることに気づき,以下に示すようにベースクラスに抽出して置くことができる.
class B
{
public:
  virtual void f();
};

void B::f()
{
  //f       ..
}

class D1: public B {...};
class D2: public B {...};

ここには2時があります.ベースクラスのf()には、D 1とD 2がfの実装について同じであるため、コード冗長性を回避するための関数定義がある.f()は虚関数であり,将来派生クラスがf()を異なる実装方式で必要とする場合,fを再定義できることを考慮する.
今まですべて順調だった.さて、もう一人の同僚が派生クラスD 3を追加したと仮定すると、f()に異なる実現方法が要求される.しかし、彼はfを再定義することを忘れた.これは災難だ.
class D3: public B {...};
B* b = new D3;
// f          !
b->f();

なお、この問題の本質は、ベースクラスにデフォルトの実装がないということではなく、派生クラスがベースクラスのデフォルトの実装を使用する必要がある場合は、明示的に示さなければならないということである.したがって,派生クラス定義fを強制できる方法が必要であり,答えは純虚関数である.
class B
{
public:
  virtual void f() = 0;
};

class D3: public B {...};
B* b = new D3; //     :        
b->f();

純粋な虚関数を持つクラスは抽象クラスであり,抽象クラスはインスタンス化できないことを知っている.D 3がfを実現しなければ、D 3もインスタンス化できない.これは、インスタンス化される各サブクラスが、純粋な虚関数fを表示的に実装しなければならないことを要求する.一方、fのデフォルト実装はどこに置きますか?1つの方法は、デフォルトの実装を別の非虚関数としてベースクラスに配置することであり、もちろん保護されています.これにより、外部が直接アクセスすることができず、派生クラスはこの関数を呼び出すことでデフォルトの動作を達成することができます.
class B
{
public:
  virtual void f() = 0;
protected:
  void f_impl();
};

void B::f_impl()
{
  //     ..
}

// D2  
class D1: public B
{
public:
  virtual void f()
};

void D1::f()
{
  //                 ,             
  f_impl();
}

class D3: public B
{
public:
  virtual void f()
};

void D3::f()
{
  //     ..
}

第2の方法は,デフォルトの実装をベースクラスの純虚関数の関数体に入れることである.そう、純粋な虚関数には関数定義が存在し、虚関数のデフォルトの実装方法を提供する役割を果たすことができます.一般的な虚関数とは異なり、派生クラスは純粋な虚関数を再定義する必要があります.デフォルトのインプリメンテーションを使用する必要がある場合は、ベースクラスの関連関数も明示的に呼び出す必要があります.
class B
{
public:
  virtual void f()
};

void B::f()
{
  //     ..
}

// D2  
class D1: public B
{
public:
  virtual void f()
};

void D1::f()
{
  //            
  B::f();
}

この方法の利点は、クライアントコードがベースクラスのデフォルト実装を直接呼び出すことができるという欠点で、メンテナンスが必要な関数が1つ少なくなることです.
B* b = new D1;
b->B::f();

まとめ:
  • の一般的な虚関数は、インタフェースとデフォルトの実装を提供する.派生クラス継承インタフェースは、デフォルトのインプリメンテーション(無料の昼食)を自動的に継承したり、関数を再定義して独自の動作を特化したりすることができます.リスクは、後でこの動作をカスタマイズする必要がある派生クラスが、再定義を忘れたためにデフォルトのインプリメンテーションを誤って使用する可能性があることです.
  • 純虚関数はインタフェースのみを提供します.しかし、関数体を有する純粋な虚関数は、デフォルトの実装も提供する.派生クラスは、インタフェースを明示的に定義する必要があります.ベースクラス関数を明示的に呼び出すことで、デフォルトの動作を完了できます.
  • は完全であるため、ついでに、公有非虚関数はインタフェースと強制実装を提供する.すなわち,すべての派生クラスはこのインタフェースとその実装を継承しなければならない.