#34 インターフェースの継承と実装の継承を区別する.

Effective C++ 6章,34項の内容です.

関数の種類

関数の定義の仕方には下記の3種類があって,目的はそれぞれ次のようになります.

  1. 純粋仮想関数:派生クラスの作成者にインターフェースの実装を強制する.デフォルトの実装を提供することも可能であるが,基本的には宣言だけで定義はなし.派生クラスで実装することが強制される.
  2. 仮想関数  :派生クラスの作成者にインターフェースの継承を強制する.基底クラスにおいてデフォルトの実装が必要であり,派生クラスで基底クラスの実装をデフォルトとして使うこともできる.
  3. 非仮想関数 :派生クラスの作成者にインターフェースの継承を強制する.ただし,実装は変えてはならないし,基底クラス・派生クラスの双方で関数の振る舞いが適切である必要がある.

それぞれの関数とコンパイル

ということで,早速サンプルコードを書いてみました.

1.純粋仮想関数

純粋仮想関数が,”派生クラスの作成者にインターフェースの実装を強制する.デフォルトの実装を提供することも可能であるが,基本的には宣言だけで定義はなし.派生クラスで実装することが強制される.”であるかどうかを見ていきます.

#include <iostream>

class AbstractClassWithPure {
public:
  virtual ~AbstractClassWithPure() {};
  virtual void pure_virtual_func() = 0;
};

class DerivedClassWithPure : public AbstractClassWithPure{
public:
};

int main() {
  DerivedClassWithPure dc;
  return 0;
}

上記のサンプルコードでは,純粋仮想関数を保持するクラスを例にあげてます.上記のコードをコンパイルしようとしても,”純粋仮想関数は派生クラスでの実装が強制される”ので,コンパイルエラーになります.

effective_cpp/chap_6th/src/chap_6th_34_1.cpp: In function ‘int main()’:
effective_cpp/chap_6th/src/chap_6th_34_1.cpp:16:24: error: cannot declare variable ‘dc’ to be of abstract type ‘DerivedClassWithPure’
   DerivedClassWithPure dc;
                        ^
effective_cpp/chap_6th/src/chap_6th_34_1.cpp:10:7: note:   because the following virtual functions are pure within ‘DerivedClassWithPure’:
 class DerivedClassWithPure : public AbstractClassWithPure{
       ^
effective_cpp/chap_6th/src/chap_6th_34_1.cpp:7:16: note:         virtual void AbstractClassWithPure::pure_virtual_func()
   virtual void pure_virtual_func() = 0;

ということで,派生クラスに関数を宣言します.

#include <iostream>

class AbstractClassWithPure {
public:
  virtual ~AbstractClassWithPure() {};
  virtual void pure_virtual_func() = 0;
};

class DerivedClassWithPure : public AbstractClassWithPure{
public:
};

void DerivedClassWithPure::pure_virtual_func() {
  std::cout << "DerivedClassWithPure::pure_virtual_func()" <<std::endl;
}

上記の様に宣言を追加しても,まだコンパイラには怒られます.DerivedClassWithPureの宣言に仮想関数として宣言する必要があります.

effective_cpp/chap_6th/src/chap_6th_34_1.cpp:14:46: error: no ‘void DerivedClassWithPure::pure_virtual_func()’ member function declared in class ‘DerivedClassWithPure’
 void DerivedClassWithPure::pure_virtual_func() {
                                              ^
effective_cpp/chap_6th/src/chap_6th_34_1.cpp: In function ‘int main()’:
effective_cpp/chap_6th/src/chap_6th_34_1.cpp:20:24: error: cannot declare variable ‘dc’ to be of abstract type ‘DerivedClassWithPure’
   DerivedClassWithPure dc;
                        ^
effective_cpp/chap_6th/src/chap_6th_34_1.cpp:10:7: note:   because the following virtual functions are pure within ‘DerivedClassWithPure’:
 class DerivedClassWithPure : public AbstractClassWithPure{
       ^
effective_cpp/chap_6th/src/chap_6th_34_1.cpp:7:16: note:         virtual void AbstractClassWithPure::pure_virtual_func()
   virtual void pure_virtual_func() = 0;

ということとで,下記のように書けばOKです.

class AbstractClassWithPure {
public:
  virtual ~AbstractClassWithPure() {};
  virtual void pure_virtual_func() = 0;
};

class DerivedClassWithPure : public AbstractClassWithPure{
public:
  void pure_virtual_func();
};

void DerivedClassWithPure::pure_virtual_func() {
  std::cout << "DerivedClassWithPure::pure_virtual_func()" <<std::endl;
}
純粋仮想関数への実装の提供

純粋仮想関数には実装を提供することも可能でした.ここではそれを実験してみます.

class AbstractClassWithPure {
public:
  virtual ~AbstractClassWithPure() {};
  virtual void pure_virtual_func() = 0;
};

void AbstractClassWithPure::pure_virtual_func() {
  std::cout << "AbstractClassWithPure::pure_virtual_func()" <<std::endl;
}

class DerivedClassWithPure : public AbstractClassWithPure{
public:
};

上記の様に,純粋仮想関数に実装を提供したとしても,派生クラスで実装を提供しないとコンパイラに怒られます.ということで,下記のようにすると...

class AbstractClassWithPure {
public:
  virtual ~AbstractClassWithPure() {};
  virtual void pure_virtual_func() = 0;
};

void AbstractClassWithPure::pure_virtual_func() {
  std::cout << "AbstractClassWithPure::pure_virtual_func()" <<std::endl;
}

class DerivedClassWithPure : public AbstractClassWithPure{
public:
  void pure_virtual_func();
};

void DerivedClassWithPure::pure_virtual_func() {
 std::cout << "DerivedClassWithPure::pure_virtual_func()" <<std::endl;
 AbstractClassWithPure::pure_virtual_func();
}

無事コンパイルが通りました.Effective C++にかかれていたように,純粋仮想関数で実装を提供する形で設計すると,派生クラスの実装者が明示的に関数を呼ばなければビルドできないので,ポカミスが起こりにくそうです.

2.仮想関数

仮想関数が,”派生クラスの作成者にインターフェースの継承を強制する.基底クラスにおいてデフォルトの実装が必要であり,派生クラスで基底クラスの実装をデフォルトとして使うこともできる.”であるかどうかを見ていきましょう.

#include <iostream>

class ParentClassWithVirtual {
public:
  virtual ~ParentClassWithVirtual() {};
  virtual void virtual_func();
};

class ChildClassWithVirtual : public ParentClassWithVirtual {

};

int main() {
  ChildClassWithVirtual cc;
  cc.virtual_func();
  return 0;
}

上記サンプルコードは仮想関数の定義がないパターンですが,当然のことながらリンクエラーになります.

CMakeFiles/chap_6th_34_2.dir/src/chap_6th_34_2.cpp.o: In function `main':
effective_cpp/chap_6th/src/chap_6th_34_2.cpp:20: undefined reference to `ParentClassWithVirtual::virtual_func()'
CMakeFiles/chap_6th_34_2.dir/src/chap_6th_34_2.cpp.o: In function `ParentClassWithVirtual::~ParentClassWithVirtual()':
effective_cpp/chap_6th/src/chap_6th_34_2.cpp:6: undefined reference to `vtable for ParentClassWithVirtual'
CMakeFiles/chap_6th_34_2.dir/src/chap_6th_34_2.cpp.o:(.rodata._ZTV21ChildClassWithVirtual[_ZTV21ChildClassWithVirtual]+0x20): undefined reference to `ParentClassWithVirtual::virtual_func()'
CMakeFiles/chap_6th_34_2.dir/src/chap_6th_34_2.cpp.o:(.rodata._ZTI21ChildClassWithVirtual[_ZTI21ChildClassWithVirtual]+0x10): undefined reference to `typeinfo for ParentClassWithVirtual'
collect2: error: ld returned 1 exit status

ということで,下記のように親クラスに仮想関数の実装を追加します.

class ParentClassWithVirtual {
public:
  virtual ~ParentClassWithVirtual() {};
  virtual void virtual_func();
};

void ParentClassWithVirtual::virtual_func() {
  std::cout << "ParentClassWithVirtual::virtual_func()" << std::endl;
}

class ChildClassWithVirtual : public ParentClassWithVirtual {

};

実装を追加すると,無事コンパイルが通ります.実行結果は下記のようになって,子クラスに仮想関数の定義がない場合,親クラスの仮想関数が呼ばれています.

ParentClassWithVirtual::virtual_func()

今度は子クラスでも仮想関数を定義します.

class ParentClassWithVirtual {
public:
  virtual ~ParentClassWithVirtual() {};
  virtual void virtual_func();
};

void ParentClassWithVirtual::virtual_func() {
  std::cout << "ParentClassWithVirtual::virtual_func()" << std::endl;
}

class ChildClassWithVirtual : public ParentClassWithVirtual {
public:
  virtual void virtual_func();
};

void ChildClassWithVirtual::virtual_func() {
  std::cout << "ChildClassWithVirtual::virtual_func()" << std::endl;
}

子クラスでの定義を追加すると,実行結果が下記のようになって,子クラスで定義した仮想関数が呼ばれていることがわかります.

ChildClassWithVirtual::virtual_func()

3.非仮想関数

最後に非仮想関数が”派生クラスの作成者にインターフェースの継承を強制する.ただし,実装は変えてはならないし,基底クラス・派生クラスの双方で関数の振る舞いが適切である必要がある.”であることを見ていきますが,ここではコンパイラが警告を出すかどうかを見てみたいと思います.

#include <iostream>

class ConcreteParentClass {
public:
  virtual ~ConcreteParentClass() {}
  void concrete_method();
};

void ConcreteParentClass::concrete_method() {
  std::cout << "ConcreteParentClass::concrete_method()" << std::endl;
}

class ConcreteChildClass : public ConcreteParentClass {
public:
  void concrete_method();
};

void ConcreteChildClass::concrete_method() {
  std::cout << "ConcreteChildClass::concrete_method()" << std::endl;
}

int main() {
  ConcreteChildClass cc;
  cc.concrete_method();
  return 0;
}

上記コードは子クラスで親クラスの非仮想関数"concrete_method"をオーバーライドしています.が,コンパイルは警告なしで通りますね...GCCの警告も見てみたんですが,該当するものはなさそうで...ということで,”インターフェースの継承と実装の継承は区別しよう"でした.