C++のfinal指定子について

AI実装検定のご案内

C++のfinal指定子とは、クラスの継承仮想関数のオーバーライドを禁止するための指定子です。

C++11から導入された機能で、主に「このクラスはこれ以上継承させない」「この仮想関数はこれ以上上書きさせない」という設計意図を明確にするために使われます。

finalを適切に使うことで、意図しない継承やオーバーライドを防ぎ、クラス設計をより安全で分かりやすくできます。

目次

final指定子でできること

クラスの継承を禁止できる

クラスにfinalを指定すると、そのクラスはほかのクラスから継承できなくなります。

つまり、そのクラスを「これ以上派生させないクラス」として扱うことができます。

たとえば、あるクラスが完成された実装クラスであり、継承による拡張を想定していない場合、finalを付けることで「このクラスは継承して使うものではない」と明示できます。

これにより、利用者が誤って派生クラスを作ってしまうことを防げます。

仮想関数のオーバーライドを禁止できる

finalは、仮想関数にも使えます。

仮想関数にfinalを指定すると、その関数はさらに派生したクラスでオーバーライドできなくなります。

たとえば、基底クラスの仮想関数を派生クラスで実装したあと、その実装をこれ以上変更されたくない場合に使います。

これにより、特定の処理だけを固定し、派生クラス側で勝手に振る舞いを変えられないようにできます。

finalをクラスに使う場合

継承させたくないクラスに使う

finalをクラスに付けると、そのクラスは継承できなくなります。

これは、継承を前提としていないクラスに使うと効果的です。

たとえば、ファイル読み込み、ログ出力、設定管理、具体的なデータ処理など、すでに完成された実装として使うクラスでは、継承による拡張が不要な場合があります。

そのようなクラスにfinalを付けることで、設計者の意図を明確にできます。

意図しない継承を防げる

C++では、明示的に禁止しない限り、クラスは継承される可能性があります。

しかし、すべてのクラスが継承を前提として設計されているわけではありません。

継承を想定していないクラスを無理に継承されると、内部状態の整合性が崩れたり、想定外の使われ方をされたりすることがあります。

finalを付けておけば、そのような誤用をコンパイル時に防げます。

実装クラスで使いやすい

実務では、インターフェースや抽象基底クラスを用意し、その具体的な実装クラスにfinalを付ける設計がよく使われます。

たとえば、「保存処理」という抽象的なインターフェースがあり、その実装として「ファイル保存」「データベース保存」などのクラスを作る場合です。

このとき、インターフェースは継承を前提にしますが、具体的な実装クラスはそれ以上継承させないようにfinalを付けることがあります。

このようにすると、拡張ポイントと固定したい実装を分けて管理できます。

finalを仮想関数に使う場合

さらに下の派生クラスで上書きできなくなる

仮想関数にfinalを指定すると、その関数は以降の派生クラスでオーバーライドできなくなります。

これは、ある派生クラスで実装を確定させたい場合に便利です。

たとえば、基底クラスでは処理の枠組みだけを定義し、派生クラスで具体的な処理を実装するとします。

その派生クラスの実装をさらに別のクラスで変更されたくない場合、関数にfinalを指定します。

これにより、その関数の動作を特定の段階で固定できます。

finalを付けられるのは仮想関数だけ

メンバ関数にfinalを付ける場合、その関数は仮想関数でなければなりません。

通常のメンバ関数は、そもそもオーバーライドの対象ではありません。

そのため、通常の関数にfinalを付けることはできません。

ただし、派生クラス側でvirtualと書いていなくても、基底クラスの仮想関数をオーバーライドしている場合、その関数は仮想関数として扱われます。

そのため、基底クラスで仮想関数として宣言されている関数であれば、派生クラス側でfinalを付けることができます。

overrideとfinalの違い

overrideは正しくオーバーライドしているか確認する指定子

overrideは、派生クラスの関数が基底クラスの仮想関数を正しくオーバーライドしていることをコンパイラに確認させる指定子です。

関数名、引数、constの有無などが基底クラスの仮想関数と一致していない場合、overrideを付けているとコンパイルエラーになります。

これにより、「オーバーライドしたつもりだったが、実際には別の関数になっていた」というミスを防げます。

finalはこれ以上オーバーライドさせない指定子

一方、finalは、仮想関数をこれ以上派生クラスでオーバーライドさせないための指定子です。

つまり、overrideは「正しくオーバーライドしているか」を確認するためのものです。

それに対して、finalは「ここでオーバーライドを終わらせる」ためのものです。

この2つは目的が異なります。

overrideとfinalは併用できる

overridefinalは同時に使えます。

実務では、仮想関数にfinalを付ける場合、overrideと一緒に使うことが多いです。

このようにすることで、「基底クラスの仮想関数を正しくオーバーライドしていること」と「これ以上オーバーライドできないこと」を同時に示せます。

書く順番は、override finalでもfinal overrideでも文法上は可能です。

ただし、一般的にはoverride finalの順で書かれることが多いです。

まず「これはオーバーライドである」と示し、そのうえで「これ以上オーバーライドできない」と読む流れになるため、自然に理解しやすいからです。

finalを使うメリット

設計意図が明確になる

finalを使う最大のメリットは、クラスや関数の設計意図が明確になることです。

クラスにfinalが付いていれば、そのクラスは継承して使うものではないと分かります。

仮想関数にfinalが付いていれば、その関数の振る舞いはそこで固定されており、さらに下の派生クラスでは変更できないと分かります。

コードを読む人にとって、設計者の意図が分かりやすくなるため、保守性の向上につながります。

誤った継承やオーバーライドを防げる

継承やオーバーライドは便利な機能ですが、使い方を誤ると設計を複雑にします。

継承を想定していないクラスを継承したり、変更してほしくない関数をオーバーライドしたりすると、想定外の不具合につながることがあります。

finalを使えば、そのような誤用をコンパイル時に防げます。

実行時ではなくコンパイル時にエラーとして検出できるため、安全性の高い設計にしやすくなります。

保守性が上がる

継承可能なクラスは、将来の変更時に派生クラスへの影響を考慮する必要があります。

特に、公開ライブラリや大規模なプロジェクトでは、どのクラスが継承される可能性があるのかを常に意識しなければなりません。

finalを使って継承できないことを明示しておけば、クラスの使われ方を限定できます。

その結果、将来的な修正やリファクタリングがしやすくなる場合があります。

最適化につながる可能性がある

finalは、コンパイラの最適化に役立つ場合があります。

仮想関数は通常、実行時にどの関数を呼び出すかが決まります。

しかし、関数やクラスがfinalであることが分かっていれば、コンパイラは「これ以上別の実装に差し替わることはない」と判断しやすくなります。

その結果、仮想関数呼び出しを通常の関数呼び出しのように最適化できる場合があります。

ただし、これは常に保証されるものではありません。

最適化の効果は、コンパイラの種類、最適化オプション、コードの構造、リンク時最適化の有無などによって変わります。

そのため、finalはパフォーマンス目的だけで使うのではなく、まずは設計意図を明確にするために使うのが自然です。

finalを使うべきケース

継承を想定していないクラス

そのクラスを継承して使う予定がない場合は、finalを付ける候補になります。

特に、値を表すクラス、ユーティリティクラス、設定管理クラス、具体的な処理を担当する実装クラスなどは、継承を前提にしないことが多いです。

このようなクラスにfinalを付けることで、余計な継承を防ぎ、使い方を明確にできます。

動作を固定したい実装クラス

インターフェースや抽象基底クラスを継承して作った具体的な実装クラスでは、finalを付けることがあります。

これは、その実装クラスをさらに継承して振る舞いを変えられることを防ぐためです。

たとえば、ある処理の具体的な実装として完成しているクラスであれば、さらに派生クラスを作られると、設計が複雑になる場合があります。

そのようなときにfinalを付けると、実装の責任範囲を明確にできます。

変更されたくない仮想関数

派生クラスで実装した仮想関数のうち、さらに下のクラスで変更されたくないものにはfinalを付けるとよいです。

たとえば、処理の手順や重要なロジックを固定したい場合です。

このようにすると、派生クラスによって一部の重要な動作が勝手に変更されることを防げます。

ライブラリやフレームワークの内部クラス

ライブラリやフレームワークを作る場合、内部的に使うクラスを外部から継承されると、将来の変更が難しくなることがあります。

そのような内部クラスにfinalを付けておけば、利用者に対して「このクラスは継承して拡張するものではない」と示せます。

公開APIとして拡張してよいクラスと、内部実装として固定したいクラスを分けるうえでも有効です。

finalを使わない方がよいケース

拡張を前提とした基底クラス

派生クラスを作ることを前提に設計しているクラスには、finalを付けるべきではありません。

たとえば、図形、プラグイン、デバイス、ストレージ、イベントハンドラなど、複数の派生クラスを作って使う設計では、基底クラスは継承されることが前提です。

このようなクラスをfinalにしてしまうと、そもそもの設計が成り立たなくなります。

テストでモック化したいクラス

単体テストで継承を使ってモックやスタブを作る設計の場合、finalを付けるとテストしにくくなることがあります。

特に、既存のクラスを派生させてテスト用の振る舞いに差し替える設計では、finalが制約になります。

ただし、これは設計次第です。

インターフェースを別に用意し、そのインターフェースを通じて依存性を注入する設計にすれば、実装クラスがfinalでもテストしやすくできます。

利用者による拡張を許可したいクラス

ライブラリやフレームワークでは、利用者がクラスを継承して独自の機能を追加したい場合があります。

そのような拡張ポイントとして提供するクラスにfinalを付けると、利用者の自由度を下げてしまいます。

そのため、公開APIでは「継承してよいクラス」と「継承してほしくないクラス」を明確に分けることが大切です。

finalとデストラクタの関係

finalクラス自体に仮想デストラクタが必要とは限らない

finalを付けたクラスは、それ以上継承されません。

そのため、そのクラスを基底クラスとして派生クラスを削除する状況は基本的にありません。

単独で使うfinalクラスであれば、通常は仮想デストラクタを用意する必要はありません。

基底クラス経由で扱う場合は基底クラスの仮想デストラクタが重要

一方で、finalクラスが何らかの基底クラスを継承している場合は、話が変わります。

そのfinalクラスを基底クラスのポインタやスマートポインタ経由で扱うなら、基底クラス側のデストラクタを仮想デストラクタにしておくことが重要です。

つまり、問題になるのは「finalクラスだから仮想デストラクタが必要かどうか」ではありません。

重要なのは、基底クラス経由でオブジェクトを削除する可能性があるかどうかです。

基底クラスのポインタを通じて派生クラスのオブジェクトを扱う設計では、基底クラスに仮想デストラクタを用意するのが一般的です。

finalとセキュリティの関係

意図しない振る舞いの変更を防ぐ助けになる

finalは、継承やオーバーライドによって意図しない振る舞いに変更されることを防ぐ助けになります。

たとえば、重要な処理を担当するクラスや関数が、派生クラスで勝手に変更されることを避けたい場合に役立ちます。

その意味では、設計上の安全性を高める要素のひとつといえます。

finalだけでセキュリティが保証されるわけではない

ただし、finalを付けたからといって、セキュリティが保証されるわけではありません。

finalはあくまで、継承やオーバーライドを制限するための言語機能です。

認証、権限管理、入力検証、例外処理、メモリ管理などの安全性は、プログラム全体の設計によって確保する必要があります。

そのため、finalはセキュリティ対策そのものではなく、意図しない拡張を防ぐための補助的な仕組みとして考えるのが適切です。

finalと抽象クラス

抽象クラスにfinalを付けると実用性が低い

純粋仮想関数を持つ抽象クラスにfinalを付けることは、通常は適切ではありません。

抽象クラスは、そのままではインスタンス化できません。

さらにfinalを付けると、その抽象クラスを継承して純粋仮想関数を実装することもできなくなります。

つまり、インスタンス化もできず、継承して実装することもできないクラスになってしまいます。

文法上は書ける場合がありますが、通常の設計ではほとんど使い道がありません。

抽象基底クラスを継承した実装クラスにfinalを付けるのは自然

一方で、抽象基底クラスを継承した具体的な実装クラスにfinalを付けるのは自然です。

抽象基底クラスは拡張ポイントとして残し、具体的な実装クラスはそれ以上継承させない、という設計です。

このようにすると、抽象的なインターフェースと具体的な実装をきれいに分離できます。

finalは予約語ではない

finalは文脈によって特別な意味を持つ

C++のfinalは、通常の予約語ではありません。

特定の文脈で使われたときに、継承禁止やオーバーライド禁止という特別な意味を持ちます。

そのため、場所によっては変数名や関数名などの識別子として使うこともできます。

識別子として使うのは避けた方がよい

文法上はfinalを識別子として使える場合がありますが、実務では避けた方がよいです。

理由は、読み手が混乱しやすいからです。

finalという名前を見ると、多くのC++プログラマーは継承禁止やオーバーライド禁止を連想します。

そのため、変数名や関数名として使うと、コードの意図が分かりにくくなります。

finalとJavaのfinalの違い

C++のfinalは用途が限定されている

C++のfinalは、主にクラスの継承禁止と仮想関数のオーバーライド禁止に使います。

変数の再代入を禁止する目的では使いません。

Javaのfinalは変数にも使える

Javaのfinalは、クラスやメソッドだけでなく、変数にも使えます。

Javaでは、変数にfinalを付けることで、その変数への再代入を禁止できます。

一方、C++で変数の再代入を禁止したい場合は、finalではなくconstを使います。

この点は、Java経験者がC++を学ぶときに混同しやすい部分です。

finalを使うときの注意点

乱用すると拡張性が下がる

finalは便利ですが、使いすぎると拡張性が下がります。

将来的に継承して使いたくなる可能性があるクラスにfinalを付けてしまうと、あとから設計を変更しにくくなります。

特に、ライブラリやフレームワークの公開APIでは、利用者がどのように拡張したいかを考慮する必要があります。

設計意図がある場合に使う

finalは、単に「何となく付ける」ものではありません。

「このクラスは継承させたくない」
「この関数はこれ以上オーバーライドさせたくない」
「この実装はここで固定したい」

このような明確な設計意図がある場合に使うのが適切です。

パフォーマンス目的だけで使わない

finalによってコンパイラの最適化がしやすくなる場合はあります。

しかし、必ず高速化されるわけではありません。

そのため、パフォーマンス目的だけでfinalを多用するのはおすすめできません。

基本的には、設計上の意図を明確にするために使い、副次的に最適化の可能性があると考えるのがよいでしょう。

finalを理解するためのポイント

クラスに付けると継承禁止

クラスにfinalを付けると、そのクラスはほかのクラスから継承できなくなります。

継承を想定していないクラスや、具体的な実装クラスに使うと効果的です。

仮想関数に付けるとオーバーライド禁止

仮想関数にfinalを付けると、その関数はさらに派生したクラスでオーバーライドできなくなります。

特定の処理を固定したい場合に使います。

overrideとは役割が違う

overrideは、正しくオーバーライドできているかを確認するための指定子です。

finalは、それ以上オーバーライドさせないための指定子です。

両者は目的が異なりますが、同時に使うことができます。

設計を明確にするために使う

finalの本質は、設計意図を明確にすることです。

「このクラスは継承しない」
「この関数はこれ以上変更しない」

このような意図をコード上で明示できる点が、finalの大きな価値です。

まとめ

C++のfinal指定子は、クラスの継承や仮想関数のオーバーライドを禁止するための機能です。

クラスに指定すると、そのクラスを継承できなくなります。

仮想関数に指定すると、その関数をさらに派生クラスでオーバーライドできなくなります。

finalを使うことで、設計意図を明確にし、誤った継承やオーバーライドをコンパイル時に防げます。

また、場合によってはコンパイラの最適化に役立つ可能性もあります。

ただし、finalを使いすぎると拡張性が下がり、テストや再利用がしにくくなる場合があります。

そのため、finalは「このクラスや関数はこれ以上拡張させない」という明確な理由がある場合に使うのが適切です。

C++におけるfinalは、単なる文法機能ではなく、クラス設計の意図を表現するための重要な指定子です。

以上、C++のfinal指定子についてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次