閉じる

【C++】protectedの使い方を解説!継承時の挙動や他修飾子との違い

C++におけるオブジェクト指向プログラミングを学ぶ上で、カプセル化の概念は非常に重要です。

その中心的な役割を担うのが「アクセス修飾子」ですが、多くの初心者が「public」と「private」の使い分けには慣れても、「protected」の具体的な活用シーンや挙動については曖昧な理解になりがちです。

protectedは、クラスの外部には詳細を隠しつつ、継承関係にある派生クラスに対してのみアクセスを許可するという、絶妙なバランスを保つためのキーワードです。

適切に使いこなすことで、コードの再利用性を高めつつ、堅牢なクラス設計を行うことが可能になります。

本記事では、protectedの基本的な定義から、継承時の複雑なアクセス制御、さらには実務での設計指針まで、網羅的に詳しく解説していきます。

C++のアクセス修飾子におけるprotectedの立ち位置

C++には、クラスのメンバ(変数や関数)に対するアクセス権を制御するために、3つのアクセス修飾子が存在します。

まずはそれぞれの役割を整理しましょう。

アクセス修飾子の基本ルール

C++のアクセス修飾子は、そのメンバに「誰がアクセスできるか」を決定します。

  1. public:どこからでもアクセス可能です。クラスの外部(main関数など)や他のクラスから自由に利用できます。
  2. private:そのクラスの内部からのみアクセス可能です。派生クラス(子クラス)であっても直接触ることはできません。
  3. protected:そのクラスの内部、および派生クラスの内部からのみアクセス可能です。クラスの外部からはアクセスできません。

このように、protectedは「身内(継承関係)には見せるが、他人(外部)には隠す」という性質を持っています。

なぜprotectedが必要なのか

もしprotectedが存在せず、publicprivateしかなかった場合を考えてみてください。

基底クラスの特定の変数を派生クラスで利用したいとき、その変数をpublicにするしかありません。

しかし、publicにすると全く関係のない外部プログラムからも値を書き換えられてしまうリスクが生じます。

逆にprivateに設定すると、派生クラスでその変数を利用するために、わざわざ基底クラスにpublicなゲッターやセッターを用意しなければならず、設計が冗長になります。

protectedは、カプセル化を維持しながら継承の利便性を最大化するために不可欠な存在なのです。

protectedの基本的な使い方

それでは、実際のソースコードを見て、protectedがどのように機能するのかを確認してみましょう。

派生クラスからのアクセス

以下のサンプルプログラムでは、基底クラスであるAnimalクラスにprotectedメンバを定義し、それを派生クラスであるDogクラスから操作しています。

C++
#include <iostream>
#include <string>

// 基底クラス
class Animal {
protected:
    // 派生クラスからはアクセスできるが、外部からは隠したいデータ
    std::string species;

public:
    Animal(std::string s) : species(s) {}

    void speak() {
        std::cout << "I am an animal of species: " << species << std::endl;
    }
};

// 派生クラス
class Dog : public Animal {
public:
    // 基底クラスのコンストラクタを呼び出す
    Dog() : Animal("Canine") {}

    void bark() {
        // protectedメンバであるspeciesに直接アクセス可能
        std::cout << "Woof! I am a " << species << "!" << std::endl;
    }
};

int main() {
    Dog myDog;
    myDog.speak(); // publicメンバなのでアクセス可能
    myDog.bark();  // publicメンバなのでアクセス可能

    // 以下の行のコメントを外すとコンパイルエラーになります
    // myDog.species = "Cat"; 
    // エラー内容: 'std::string Animal::species' is protected within this context

    return 0;
}
実行結果
I am an animal of species: Canine
Woof! I am a Canine!

このプログラムにおいて、speciesprotectedとして宣言されています。

そのため、Dogクラスのメンバ関数であるbark()の中では自由に読み書きができます。

しかし、main関数の中でmyDog.speciesと直接指定しようとすると、コンパイラはアクセス違反としてエラーを報告します。

これがprotectedの最も基本的な挙動です。

継承の型によるprotectedの挙動変化

C++の継承には、public継承、protected継承、private継承の3種類があります。

これらによって、基底クラスのprotectedメンバが派生クラスでどのような扱いになるかが変わります。

継承の種類とアクセス権の変換表

以下の表は、基底クラスのメンバが継承後に派生クラスでどのアクセス権になるかをまとめたものです。

基底クラスのメンバ権限public継承protected継承private継承
publicpublicprotectedprivate
protectedprotectedprotectedprivate
privateアクセス不可アクセス不可アクセス不可

一般的に利用されるのはpublic継承ですが、特定の設計意図がある場合には他の継承方法も検討されます。

1. public継承の場合

基底クラスのprotectedメンバは、派生クラスでもそのままprotectedとして扱われます。

これにより、さらにその下の孫クラスからもアクセスが可能になります。

最も一般的で直感的な挙動です。

2. protected継承の場合

基底クラスのpublicメンバとprotectedメンバの両方が、派生クラスではすべてprotectedに格下げされます。

これにより、派生クラスの外部からは何も見えなくなりますが、さらに継承する孫クラスからはアクセスできる状態を維持します。

3. private継承の場合

基底クラスのすべてのメンバが、派生クラスではprivateになります。

これは「実装のための継承」と呼ばれ、基底クラスの機能を派生クラスの内部でのみ利用し、それ以降の継承先(孫クラス)や外部には一切公開したくない場合に使用されます。

実践的な活用シーン:Template Methodパターン

protectedが最も威力を発揮するのは、デザインパターンの1つである「Template Method(テンプレートメソッド)パターン」を実装するときです。

このパターンでは、基底クラスで「アルゴリズムの骨組み」を定義し、その中の具体的なステップ(詳細な処理)を派生クラスに任せます。

このとき、詳細な処理を行う関数をprotectedにすることで、外部からは呼び出せないが、派生クラスでは自由にカスタマイズ(オーバーライド)できるように設計します。

Template Methodパターンの実装例

C++
#include <iostream>

// 基底クラス:データ処理の枠組みを提供
class DataProcessor {
public:
    // テンプレートメソッド:外部から呼び出すメイン処理
    // この流れは固定したいので非仮想関数にすることが多い
    void process() {
        readData();     // ステップ1
        formatData();   // ステップ2
        saveData();     // ステップ3
    }

    virtual ~DataProcessor() {}

protected:
    // 派生クラスに実装を任せたいステップ(protected)
    // 外部からは直接呼ぶ必要がないため、隠蔽する
    virtual void readData() = 0;
    virtual void formatData() = 0;

    // 共通の処理は基底クラスで実装してもよい
    void saveData() {
        std::cout << "Saving data to database..." << std::endl;
    }
};

// 派生クラス:CSV形式のデータを扱う
class CsvProcessor : public DataProcessor {
protected:
    void readData() override {
        std::cout << "Reading data from CSV file." << std::endl;
    }

    void formatData() override {
        std::cout << "Formatting CSV data to internal format." << std::endl;
    }
};

int main() {
    DataProcessor* processor = new CsvProcessor();

    // 外部からは一連の流れを呼び出すだけ
    processor->process();

    // 以下の個別のステップはprotectedなので外部から呼べない(安全)
    // processor->readData(); // エラー

    delete processor;
    return 0;
}
実行結果
Reading data from CSV file.
Formatting CSV data to internal format.
Saving data to database...

この設計の素晴らしい点は、readDataformatDataといった「部品」となる関数を、外部の利用者が誤って単体で呼び出すのを防げる点にあります。

一方で、新しいデータ形式(例えばJSONなど)に対応させたい開発者は、DataProcessorを継承してこれらのprotected関数を実装するだけで、安全に機能を拡張できます。

protectedを使用する際の注意点と設計指針

protectedは便利な一方で、多用しすぎると「密結合」という問題を引き起こす可能性があります。

ここでは、設計時に気をつけるべきポイントを解説します。

1. カプセル化の破壊(密結合)に注意

protectedメンバ変数を多用すると、派生クラスが基底クラスの内部実装に強く依存することになります。

もし将来的に基底クラスの変数の型を変更したり、変数自体を削除したりすると、すべての派生クラスのコードを修正しなければならなくなる恐れがあります。

これを避けるためには、メンバ変数をprotectedにするのではなく、変数はprivateに保ち、protectedなアクセス用関数(プロテクト・ゲッター/セッター)を提供するという手法が推奨されます。

2. インターフェースと実装の分離

protected関数は「派生クラスのためのインターフェース」です。

  • public: 全世界に向けたインターフェース
  • protected: 子孫に向けたインターフェース
  • private: 自分自身のための実装

このように明確に役割を分けることで、どこまでが変更の影響範囲かを把握しやすくなります。

3. virtual関数との組み合わせ

前述のTemplate Methodパターンのように、protectedvirtual(仮想関数)は非常に相性が良いです。

「派生クラスで必ず実装してほしいが、外部には見せたくない」という意図を、protected virtual(または純粋仮想関数 = 0)という形で表現できます。

protected vs 他の修飾子:使い分けの判断基準

どの修飾子を使うべきか迷った際は、以下のフローチャートをイメージしてください。

判断基準のまとめ

状況推奨される修飾子理由
APIとして公開する機能public利用者がそのクラスを使うための窓口。
派生クラスで共通利用するユーティリティprotected外部には隠し、コードの再利用性を高める。
派生クラスでカスタマイズしてほしい処理protected virtualポリモーフィズムを活用しつつカプセル化を守る。
クラス内部の計算や一時的な状態保持private外部や継承先からの予期せぬ変更を防ぐ。

基本的には「最も制限の強いもの(private)から検討する」のが、クリーンな設計のコツです。

最初から何でもprotectedにするのではなく、必要に迫られた時に初めてprivateからprotectedへ昇格させるようにしましょう。

フレンド関数とprotectedの関係

少し発展的な内容として、friendキーワードとの関係にも触れておきます。

friendとして指定された関数やクラスは、通常のアクセス制限を無視してprivateprotectedメンバにアクセスできます。

C++
class Base {
protected:
    int secretValue = 42;
    friend void revealSecret(Base& b); // フレンド関数の宣言
};

void revealSecret(Base& b) {
    // 継承関係になくても、friendならprotectedにアクセスできる
    std::cout << "The secret is: " << b.secretValue << std::endl;
}

ただし、friendはカプセル化を意図的に破る強力な機能であるため、多用は禁物です。

ユニットテストで内部状態を確認したい場合や、密接に関連する2つのクラス(例えばIteratorContainer)の間でのみ使用するのが一般的です。

まとめ

C++におけるprotectedは、継承を通じたクラスの拡張性と、外部からの隠蔽を両立させるための重要なアクセス修飾子です。

publicが「何ができるか(サービスの公開)」を表し、privateが「どう実装しているか(秘密の保持)」を表すのに対し、protected「継承先でどう振る舞うべきか(家系のルール)」を定義するものだと言えます。

本記事で解説した継承時のアクセス権の変化や、Template Methodパターンのような実戦的なテクニックを理解することで、より洗練されたクラス設計が可能になります。

まずは、メンバ変数を安易にpublicにせず、継承を意識した設計が必要になった際にprotectedを正しく配置することから始めてみてください。

適切なアクセス制御こそが、大規模でメンテナンス性の高いC++プログラムを構築するための第一歩となります。

クラスの定義と基本

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!