C++の継承コンストラクタとは、基底クラスのコンストラクタを派生クラスの初期化でも使えるようにする機能です。
通常、C++では基底クラスのメンバ関数は派生クラスに継承されますが、コンストラクタは通常のメンバ関数のようには継承されません。
そのため、基底クラスに引数付きコンストラクタがあっても、派生クラス側で何も定義しなければ、その引数を使って派生クラスのオブジェクトを作れるとは限りません。
この問題を簡潔に解決するために使われるのが、継承コンストラクタです。
継承コンストラクタを使うと、基底クラスにある複数のコンストラクタを、派生クラス側でもまとめて利用できるようになります。
特に、派生クラスが基底クラスに対して薄いラッパーのような役割を持つ場合に便利です。
コンストラクタは通常の継承では引き継がれない
メンバ関数とコンストラクタは扱いが違う
C++では、基底クラスに定義された通常のメンバ関数は、派生クラスからも利用できます。
しかし、コンストラクタは少し特別です。
コンストラクタは「そのクラス自身のオブジェクトを初期化するための関数」なので、基底クラスのコンストラクタがそのまま派生クラスのコンストラクタになるわけではありません。
たとえば、基底クラスに整数を受け取るコンストラクタがあったとしても、派生クラスに何も書かなければ、派生クラスが同じ整数引数で構築できるとは限りません。
従来は転送用のコンストラクタを書く必要があった
継承コンストラクタがない場合、派生クラス側で基底クラスのコンストラクタへ引数を渡すためのコンストラクタを明示的に書く必要があります。
このようなコンストラクタは、派生クラス自身では特別な処理をせず、単に基底クラスのコンストラクタへ引数を渡すだけの役割になります。
基底クラスのコンストラクタが1つだけなら大きな負担ではありません。
しかし、基底クラスに複数のコンストラクタがある場合、派生クラス側でも同じような転送コンストラクタをいくつも書く必要があり、コードが冗長になります。
継承コンストラクタは、このような冗長な記述を減らすために役立ちます。
継承コンストラクタの基本的な考え方
基底クラスのコンストラクタを派生クラスの初期化候補にする
継承コンストラクタを使うと、基底クラスのコンストラクタを派生クラスの初期化時にも利用できます。
ただし、厳密には「基底クラスのコンストラクタが派生クラス内に完全にコピーされる」と考えるより、派生クラスを初期化するときに、基底クラスのコンストラクタが候補として見えるようになると理解するほうが正確です。
この機能によって、派生クラスに明示的なコンストラクタを書かなくても、基底クラスと同じ引数形式で派生クラスのオブジェクトを作れるようになります。
作られるのは派生クラスのオブジェクト
継承コンストラクタを使った場合でも、作られるのはあくまで派生クラスのオブジェクトです。
基底クラスのオブジェクトだけが作られるわけではありません。
派生クラスのオブジェクトの中にある「基底クラス部分」が、基底クラスのコンストラクタによって初期化されます。
つまり、派生クラスのオブジェクト全体が作られ、その中の基底クラス部分の初期化に、基底クラスのコンストラクタが使われるということです。
継承コンストラクタで初期化される範囲
基底クラス部分は基底クラスのコンストラクタで初期化される
継承コンストラクタが選ばれた場合、基底クラス部分は対応する基底クラスのコンストラクタによって初期化されます。
たとえば、基底クラスに整数を受け取るコンストラクタがあり、それを継承コンストラクタとして利用した場合、派生クラスの中にある基底クラス部分がその整数引数によって初期化されます。
この点は、派生クラス側で明示的にコンストラクタを書いて、初期化リストで基底クラスのコンストラクタを呼び出す場合と近いイメージです。
派生クラス独自のメンバは自動で意味のある値にはならない
継承コンストラクタを使うときに特に注意すべきなのが、派生クラス独自のメンバ変数です。
基底クラス部分は基底クラスのコンストラクタで初期化されますが、派生クラス側で追加したメンバ変数は、それだけで自動的に意味のある値へ初期化されるわけではありません。
派生クラスに独自の整数メンバなどがある場合、そのメンバにデフォルト値を与えていなければ、不定値になる可能性があります。
これは実務上かなり重要な注意点です。
継承コンストラクタを使う場合でも、派生クラスに独自メンバがあるなら、そのメンバが適切に初期化されるかを必ず確認する必要があります。
デフォルトメンバ初期化子を使うと安全
派生クラス独自のメンバを安全に扱いたい場合は、デフォルトメンバ初期化子を使うのが有効です。
たとえば、整数メンバには最初から0を与える、文字列メンバには空文字列や適切な初期値を与える、といった設計にしておくと、継承コンストラクタを使った場合でも不完全な初期化を避けやすくなります。
ただし、派生クラス独自のメンバに必ず外部から値を渡す必要がある場合は、継承コンストラクタだけでは不十分です。
その場合は、派生クラス側で明示的なコンストラクタを定義するべきです。
派生クラス側のコンストラクタとの関係
同じ引数形式のコンストラクタを派生クラスに定義できる
継承コンストラクタを使っていても、派生クラス側に独自のコンストラクタを定義できます。
派生クラス側で、基底クラスから継承されるものと同じ引数形式のコンストラクタを定義した場合、派生クラス側のコンストラクタが優先されます。
つまり、継承コンストラクタは、派生クラス側で明示的に定義したコンストラクタを上書きするものではありません。
派生クラス側のコンストラクタが継承コンストラクタを隠す
派生クラスに同じシグネチャのコンストラクタがある場合、その形の継承コンストラクタは隠されます。
そのため、一部の引数形式だけ派生クラス側で特別な処理を行い、それ以外は基底クラスのコンストラクタを継承して使う、といった設計が可能です。
これは、継承コンストラクタの便利な使い方のひとつです。
コピーコンストラクタとムーブコンストラクタの扱い
基底クラスのコピーコンストラクタがそのまま派生クラス用になるわけではない
継承コンストラクタを理解するうえで、コピーコンストラクタとムーブコンストラクタの扱いは少し注意が必要です。
基底クラスにコピーコンストラクタがあるからといって、それがそのまま派生クラスのコピーコンストラクタとして継承されるわけではありません。
派生クラスのコピーコンストラクタやムーブコンストラクタは、通常の特殊メンバ関数として扱われます。
派生クラス用の特殊メンバ関数が通常どおり扱われる
派生クラスのコピーやムーブでは、派生クラス自身のコピーコンストラクタやムーブコンストラクタが必要になります。
これらは、条件を満たせば暗黙的に宣言・定義されます。
継承コンストラクタがあるからといって、派生クラス用のコピーやムーブの仕組みがなくなるわけではありません。
そのため、「基底クラスのコピーコンストラクタも継承できるから、派生クラスのコピーもそれで処理される」と考えるのは不正確です。
デフォルトコンストラクタとの関係
引数なしで構築できるかは有効なコンストラクタがあるかで決まる
継承コンストラクタを使っていても、派生クラスを引数なしで構築できるとは限りません。
引数なしで構築できるかどうかは、派生クラスを引数なしで初期化できる有効なコンストラクタがあるかどうかで決まります。
基底クラスにデフォルトコンストラクタがあり、それを使える場合は、派生クラスも引数なしで構築できる可能性があります。
一方、基底クラスに引数付きコンストラクタしかなく、引数なしで呼び出せるコンストラクタが存在しない場合、派生クラスも引数なしでは構築できません。
デフォルト引数がある場合は引数なし構築できることがある
基底クラスに明示的なデフォルトコンストラクタがなくても、すべての引数にデフォルト値が与えられているコンストラクタがある場合は、引数なしで呼び出せることがあります。
この場合、継承コンストラクタを通じて、派生クラスも引数なしで構築できる可能性があります。
そのため、「基底クラスにデフォルトコンストラクタがないから、派生クラスは必ず引数なしで作れない」と単純に考えるのは正確ではありません。
より正確には、引数なしで呼び出せるコンストラクタが存在するかどうかがポイントです。
アクセス指定子との関係
publicなコンストラクタは外部から使える
基底クラスのコンストラクタがpublicであれば、継承コンストラクタとして派生クラスの初期化に使いやすくなります。
外部のコードから派生クラスのオブジェクトを作る場合、その基底クラスのコンストラクタがアクセス可能である必要があります。
protectedやprivateのコンストラクタを勝手に公開できるわけではない
継承コンストラクタをpublicな領域に書いたとしても、基底クラスのprotectedコンストラクタやprivateコンストラクタを、外部から自由に使えるようにできるわけではありません。
基底クラス側のアクセス制御は維持されます。
つまり、基底クラスのコンストラクタがprivateであれば、派生クラス側で継承コンストラクタを使っても、通常の外部コードからそのコンストラクタを利用することはできません。
protectedの場合も、通常の外部コードからは使えません。
ただし、クラス内部やフレンドなど、アクセスが許される文脈では扱いが変わることがあります。
using宣言の位置だけでアクセス制御を変えられるわけではない
通常のメンバ関数に対するusing宣言では、アクセス指定を変えられるように見える場面があります。
しかし、継承コンストラクタでは、基底クラスのコンストラクタのアクセス制御が重要になります。
そのため、publicな領域に継承コンストラクタの宣言を書いたからといって、基底クラスのprivateコンストラクタがpublic化されるわけではありません。
explicitとの関係
基底クラスのexplicitは維持される
基底クラスのコンストラクタがexplicitである場合、その性質は継承コンストラクタを通じても維持されます。
つまり、基底クラスのコンストラクタが暗黙変換に使えないように設計されているなら、派生クラスでも同じように暗黙変換には使えません。
これは型安全性の観点から重要です。
暗黙変換を許すかどうかは基底クラスの設計に影響される
継承コンストラクタを使うと、派生クラスの初期化方法が基底クラスのコンストラクタ設計に強く影響されます。
基底クラスのコンストラクタがexplicitなら派生クラスでも明示的な構築が必要になります。
一方、基底クラスのコンストラクタがexplicitでなければ、派生クラスでも暗黙変換が可能になる場面があります。
そのため、継承コンストラクタを使う場合は、基底クラスのコンストラクタがどのような変換を許しているかも確認する必要があります。
デフォルト引数との関係
基底クラスのデフォルト引数も利用できる
基底クラスのコンストラクタにデフォルト引数がある場合、継承コンストラクタでもその性質を利用できます。
たとえば、基底クラスのコンストラクタが2つの引数を取るものの、2つ目にデフォルト値がある場合、派生クラスでも1つの引数だけで構築できる可能性があります。
デフォルト引数は構築方法の幅を広げる
デフォルト引数があるコンストラクタを継承すると、派生クラスでも複数の呼び出し方が可能になります。
これは便利ですが、同時に「派生クラスとして許したい初期化方法が増える」ということでもあります。
基底クラスと同じ初期化方法を派生クラスにも許して問題ない場合は有効です。
しかし、派生クラスでは初期化方法を制限したい場合、継承コンストラクタをそのまま使うと設計意図に合わない可能性があります。
多重継承における注意点
他の基底クラスも初期化できなければならない
多重継承で継承コンストラクタを使う場合、選ばれたコンストラクタの継承元となる基底クラスだけでなく、他の基底クラスも初期化される必要があります。
たとえば、ある基底クラスのコンストラクタを使って派生クラスを構築しようとした場合、もう一方の基底クラスは通常どおりデフォルト初期化されます。
そのため、他の基底クラスがデフォルト構築できない場合、派生クラス全体を構築できずエラーになります。
同じ引数形式のコンストラクタがあると曖昧になる
複数の基底クラスが同じ引数形式のコンストラクタを持っており、それらを両方とも継承した場合、派生クラスの構築が曖昧になることがあります。
たとえば、2つの基底クラスがどちらも整数を受け取るコンストラクタを持っている場合、派生クラスを整数1つで構築しようとすると、どちらの基底クラス由来のコンストラクタを使うべきか判断できません。
このような場合は、派生クラス側で明示的にコンストラクタを定義し、各基底クラスをどのように初期化するかをはっきり書くべきです。
多重継承では明示的なコンストラクタのほうが読みやすい場合が多い
多重継承では、初期化対象が複数になります。
そのため、継承コンストラクタを使うと、どの基底クラスがどのように初期化されるのかが分かりにくくなることがあります。
特に、複数の基底クラスが同じようなコンストラクタを持っている場合は、明示的なコンストラクタを書いたほうが安全で読みやすくなります。
仮想継承における注意点
仮想基底クラスは最も派生したクラスが初期化する
仮想継承では、仮想基底クラスの初期化責任は最も派生したクラスにあります。
これは通常の継承よりも初期化のルールが複雑です。
継承コンストラクタと仮想継承を組み合わせると、どのクラスがどの基底部分を初期化するのかが分かりにくくなることがあります。
可読性を優先するなら明示的に書くほうが安全
仮想継承を使う設計は、それ自体が比較的複雑になりやすいです。
そこに継承コンストラクタを組み合わせると、初期化の流れを追うのが難しくなる場合があります。
そのため、仮想継承が関係する場合は、継承コンストラクタで簡潔に済ませるよりも、派生クラス側で明示的にコンストラクタを書くほうが分かりやすいことがあります。
初期化順序との関係
継承コンストラクタを使っても初期化順序は変わらない
継承コンストラクタを使っても、C++の基本的な初期化順序は変わりません。
初期化は、おおむね次の順序で行われます。
まず仮想基底クラスが初期化され、次に直接基底クラスが初期化されます。
その後、メンバ変数が宣言順に初期化され、最後にコンストラクタ本体が実行されます。
継承コンストラクタは、基底クラス部分の初期化方法に関わる機能ですが、C++全体の初期化順序そのものを変更するものではありません。
メンバ変数の初期化順序にも注意が必要
派生クラスに複数のメンバ変数がある場合、それらはコンストラクタの記述順ではなく、クラス内で宣言された順に初期化されます。
これは継承コンストラクタを使う場合でも同じです。
そのため、メンバ同士に依存関係がある場合は、宣言順にも注意する必要があります。
集成体初期化との関係
継承コンストラクタを持つと集成体ではなくなる可能性が高い
C++では、単純なデータ構造を集成体として初期化できる場合があります。
しかし、継承コンストラクタを持つクラスは、集成体の条件から外れる可能性があります。
特にC++20以降では、継承コンストラクタを持たないことが集成体の条件に含まれます。
そのため、集成体初期化を前提にした単純な構造体に継承コンストラクタを追加すると、初期化方法が変わる可能性があります。
単純なデータ保持型では慎重に使う
単純なデータ保持を目的とした型では、継承コンストラクタを使うよりも、集成体初期化を活かしたほうが分かりやすい場合があります。
特に、構造体を軽量なデータコンテナとして使いたい場合は、継承コンストラクタを追加することで、かえって扱いが複雑になることがあります。
継承コンストラクタが便利な場面
薄いラッパークラス
継承コンストラクタは、派生クラスが基底クラスに対して薄いラッパーのような役割を持つ場合に便利です。
たとえば、基底クラスにほとんどの状態や処理があり、派生クラスは型名を分けるためだけに存在するようなケースです。
この場合、派生クラス側で基底クラスと同じコンストラクタを何度も書くのは冗長です。
継承コンストラクタを使えば、基底クラスと同じ初期化方法を簡潔に利用できます。
独自例外クラス
継承コンストラクタの典型的な利用例として、標準例外クラスを継承した独自例外クラスがあります。
標準ライブラリの例外クラスには、エラーメッセージを受け取るコンストラクタなどが用意されています。
独自例外クラスがそれらと同じ初期化方法をそのまま使いたい場合、継承コンストラクタは非常に便利です。
このようなケースでは、派生クラスに独自メンバを追加しないことも多いため、継承コンストラクタとの相性が良いです。
基底クラスの初期化方法をそのまま公開してよい型
継承コンストラクタが向いているのは、基底クラスの初期化方法を派生クラスでもそのまま許可して問題ない場合です。
派生クラスの利用者に対して、基底クラスと同じ作り方を提供したい場合は、継承コンストラクタが有効です。
一方で、派生クラスでは基底クラスと異なる制約を設けたい場合には、慎重に判断する必要があります。
継承コンストラクタを避けたほうがよい場面
派生クラスに独自の不変条件がある場合
派生クラスに独自の不変条件がある場合、継承コンストラクタだけで済ませるのは危険です。
不変条件とは、そのクラスのオブジェクトが常に満たしていなければならない条件のことです。
たとえば、派生クラスに追加されたメンバが必ず特定の範囲内でなければならない場合や、基底クラスの値と派生クラスの値に整合性が必要な場合、継承コンストラクタだけではそれを保証しにくくなります。
このような場合は、派生クラス側で明示的なコンストラクタを定義し、必要な検証や初期化を行うべきです。
基底クラスの構築方法をそのまま公開したくない場合
継承コンストラクタを使うと、基底クラスのコンストラクタ群が派生クラスの構築方法として利用可能になります。
これは便利な一方で、派生クラスとしては許可したくない初期化方法まで公開してしまう可能性があります。
基底クラスには複数の作り方があっても、派生クラスではその一部だけを許したい場合があります。
そのような場合は、継承コンストラクタを使わず、派生クラス側で必要なコンストラクタだけを明示的に用意するほうが安全です。
派生クラスに重要なメンバ変数がある場合
派生クラスに重要なメンバ変数がある場合も注意が必要です。
継承コンストラクタでは、基底クラス部分は適切に初期化されますが、派生クラス独自のメンバが設計どおりに初期化されるとは限りません。
デフォルトメンバ初期化子で十分な場合は問題ありません。
しかし、コンストラクタ引数に基づいて派生クラス独自のメンバを初期化する必要があるなら、明示的なコンストラクタを書くべきです。
多重継承で初期化が複雑になる場合
多重継承では、継承コンストラクタによって構築候補が増え、曖昧性が発生しやすくなります。
また、ある基底クラス由来のコンストラクタを使ったとき、他の基底クラスやメンバがどのように初期化されるのかも考慮しなければなりません。
初期化の流れが複雑になる場合は、継承コンストラクタよりも明示的なコンストラクタのほうが読みやすく、保守しやすくなります。
継承コンストラクタと転送コンストラクタの違い
転送コンストラクタより簡潔に書ける
基底クラスのコンストラクタへ引数を渡すために、派生クラス側で転送コンストラクタを書く方法もあります。
しかし、基底クラスに多くのコンストラクタがある場合、派生クラス側でそれらをすべて書くのは手間です。
継承コンストラクタを使えば、基底クラスの複数のコンストラクタをまとめて派生クラス側で利用できるため、記述量を大きく減らせます。
可変長テンプレートの転送コンストラクタより安全な場合がある
任意の引数を基底クラスへ転送するために、可変長テンプレートを使った転送コンストラクタを書くこともあります。
しかし、この方法は意図しない型まで受け取ってしまったり、コピーコンストラクタやムーブコンストラクタと競合したりすることがあります。
継承コンストラクタは、基底クラスに実際に存在するコンストラクタを利用する仕組みなので、単純な転送目的であれば、可変長テンプレートより分かりやすく安全な場合があります。
ただし、派生クラス独自の初期化処理が必要な場合は、継承コンストラクタではなく明示的なコンストラクタが必要です。
継承コンストラクタのメリット
コード量を減らせる
継承コンストラクタの大きなメリットは、コード量を減らせることです。
基底クラスのコンストラクタが複数ある場合、派生クラス側で同じ引数を受け取り、基底クラスへ渡すだけのコンストラクタを何度も書く必要がなくなります。
これにより、クラス定義が簡潔になります。
基底クラスのコンストラクタ追加に対応しやすい
基底クラスに新しいコンストラクタが追加された場合、継承コンストラクタを使っていれば、派生クラス側で個別に転送コンストラクタを追加しなくてもよい場合があります。
そのため、薄い派生クラスでは保守性が上がることがあります。
薄い派生クラスを表現しやすい
派生クラスが独自の初期化処理をほとんど持たない場合、継承コンストラクタは非常に自然です。
特に、独自例外クラスや型を区別するための薄い派生クラスでは、基底クラスの初期化方法をそのまま使いたいことが多くあります。
そのような場面では、継承コンストラクタによって意図を簡潔に表現できます。
継承コンストラクタのデメリット
初期化の見通しが悪くなることがある
継承コンストラクタを使うと、派生クラスの定義だけを見ても、どの引数で構築できるのかが分かりにくい場合があります。
利用可能なコンストラクタを把握するには、基底クラスの定義を見る必要があります。
基底クラスが大きい場合や、コンストラクタが多い場合は、派生クラスの使い方が分かりにくくなることがあります。
派生クラスの制約を表現しにくい
派生クラスに独自の制約がある場合、基底クラスのコンストラクタをそのまま公開してしまうと、その制約を守れないオブジェクトが作られる可能性があります。
クラスの不変条件を保つ必要がある場合は、継承コンストラクタだけに頼らず、派生クラス側で明示的に初期化処理を書くべきです。
意図しない構築方法を許す可能性がある
基底クラスに多くのコンストラクタがある場合、継承コンストラクタによって派生クラスでも多くの構築方法が利用できるようになります。
その中には、派生クラスとしては許したくない構築方法が含まれる可能性があります。
継承コンストラクタを使う場合は、「基底クラスのコンストラクタをすべて派生クラスの利用者に見せてもよいか」を考える必要があります。
実務での判断基準
基底クラスと同じ初期化方法で問題ないなら使いやすい
継承コンストラクタは、派生クラスが基底クラスと同じ初期化方法を持っていて問題ない場合に適しています。
特に、派生クラスに独自メンバがほとんどない場合や、基底クラスの機能を少し拡張するだけの場合は、継承コンストラクタを使うとコードがすっきりします。
派生クラスに独自状態があるなら慎重に使う
派生クラスに独自の状態がある場合は、継承コンストラクタの使用を慎重に判断する必要があります。
追加メンバがデフォルト値だけで問題ないなら、継承コンストラクタを使ってもよい場合があります。
しかし、追加メンバに必ず意味のある値を設定する必要があるなら、明示的なコンストラクタを書くべきです。
設計意図を明確にしたい場合は明示的なコンストラクタを選ぶ
継承コンストラクタは便利ですが、常に最善とは限りません。
利用者に許可する構築方法を明確にしたい場合や、初期化時に検証を行いたい場合は、派生クラス側で明示的なコンストラクタを書くほうが適しています。
簡潔さを優先するのか、設計意図の明確さを優先するのかを考えて選ぶことが重要です。
よくある誤解
基底クラスのコンストラクタが派生クラスに完全コピーされるわけではない
継承コンストラクタは、基底クラスのコンストラクタを派生クラスに完全コピーする機能ではありません。
より正確には、派生クラスの初期化時に、基底クラスのコンストラクタが候補として利用できるようになる機能です。
この違いを理解しておくと、コピーコンストラクタやムーブコンストラクタ、多重継承などで混乱しにくくなります。
派生クラスのメンバまで自動で適切に初期化されるわけではない
継承コンストラクタが初期化する中心は、基底クラス部分です。
派生クラス独自のメンバは、通常のルールに従って初期化されます。
そのため、派生クラスに追加メンバがある場合は、その初期化方法を別途考える必要があります。
privateコンストラクタをpublic化できるわけではない
継承コンストラクタをpublicな領域に書いても、基底クラスのprivateコンストラクタを外部から利用できるようにはなりません。
基底クラス側のアクセス制御は維持されます。
継承コンストラクタがあれば常に派生クラスのコンストラクタを書かなくてよいわけではない
継承コンストラクタは、基底クラスの初期化方法を再利用するための機能です。
派生クラス独自の検証、変換、追加メンバの初期化、不変条件の保証が必要な場合は、派生クラス側でコンストラクタを書く必要があります。
まとめ
継承コンストラクタは基底クラスの初期化方法を派生クラスで再利用する機能
C++の継承コンストラクタは、基底クラスのコンストラクタを派生クラスの初期化でも利用できるようにする機能です。
これにより、基底クラスのコンストラクタを派生クラス側で何度も転送定義する必要がなくなり、コードを簡潔にできます。
特に、薄いラッパークラスや独自例外クラスでは有効です。
ただし派生クラス独自の初期化には注意が必要
継承コンストラクタを使っても、派生クラス独自のメンバが自動的に適切な値になるわけではありません。
また、基底クラスの構築方法をそのまま派生クラスにも公開することになるため、派生クラスの設計意図に合っているかを確認する必要があります。
派生クラスに独自の状態や不変条件がある場合は、明示的なコンストラクタを書くほうが安全です。
使いどころを選べば便利な機能
継承コンストラクタは、正しく使えばコードを簡潔にし、保守性を高められる便利な機能です。
一方で、派生クラスの初期化設計を曖昧にしてしまう危険もあります。
基底クラスと同じ初期化方法を派生クラスでも許してよい場合は有効です。
派生クラス独自のルールを守る必要がある場合は、明示的なコンストラクタを定義するのが適切です。
以上、C++の継承コンストラクタについてでした。
最後までお読みいただき、ありがとうございました。
