こんにちは。田原です。

オブジェクト指向プログラミングの3大特長は、「隠蔽」、「継承」、「動的なポリモーフィズム」です。前回に引き続き、今回も継承について解説します。今回は、アクセス指定子の最後の1つprotectedメンバ、および、public/private/protected継承について説明します。

 

1.protectedなメンバ変数

第20回目でアクセス指定子について説明しました。
クラスの外からのアクセスを許可するpublicと、クラスの外からのアクセスを禁止するprivateです。
そして、もう一つ、クラスの外からのアクセスは禁止するが派生クラスからのアクセスは許可するprotectedがあります。

class Foo
{
    int mPrivate;
protected:
    int mProtected;
public:
    int mPublic;

    Foo() : mPrivate(1), mProtected(2), mPublic(3) { }
};

class Bar : public Foo
{
    Bar() : Foo()
    {
//      mPrivate=10;    // エラー
        mProtected=20;
        mPublic=30;
    }
};

int main()
{
    Bar bar;

//  bar.mPrivate=100;    // エラー
//  bar.mProtected=200;  // エラー
    bar.mPublic=300;
}

Wandboxで試してみる

private程ではないですが、protectedもアクセスできる範囲を狭くするため、仕様変更等を行った時の影響範囲を狭めることが出来ます。
クラス外部からアクセスする必要がないメンバはできるだけprivateとし、どうしても派生クラスだけにはアクセスさせたい場合にprotectedとすることをお勧めします。

 

2.基底クラスのアクセス指定

継承する時、基底クラス名の前にpublicと書きました。ということは、もしかして、privateやprotectedも書けそうな気がしますね。その通りです。書くことが出来ます。
 

2-1.名前のないメンバ変数のような振る舞い

前回のメモリ・イメージを見ると解るかも知れませんが、基底クラスは派生クラスの一部として存在します。それはまるで、派生クラスの中に基底クラス型の(無名の)メンバ変数を定義したかのように配置されています。
そして、実際に派生クラスの(無名の)メンバ変数のように振る舞います。

class Base
{
public:
    int mBaseVar0;
    int mBaseVar1;
    Base() : mBaseVar0(10), mBaseVar1(11) { }
};
  
class Derived : public Base
{
public:
    int mDerivedVar0;
    int mDerivedVar1;
    Derived() : mDerivedVar0(100), mDerivedVar1(101) { }
};

のDerivedをその考え方で無理っと書き直してみると、次のイメージにかなり近いです。

class Derived
{
public:
    Base (無名変数);

public:
    int mDerivedVar0;
    int mDerivedVar1;
    Derived() : mDerivedVar0(100), mDerivedVar1(101) { }
};

実際に無名のメンバ変数は定義できませんので、仮の名前をつけると次のようになります。

class Derived : public Base
{
public:
    Base b;
public:
    int mDerivedVar0;
    int mDerivedVar1;
    Derived() : mDerivedVar0(100), mDerivedVar1(101) { }
};

Wandboxで試してみる

実際のアドレスは起動する度に変わりますが、メンバ変数間の位置関係は、基底クラスとして定義した場合と同じです。メンバ変数名がないので、それを省略して直接Baseクラスのメンバ変数名で指定していると考えることができます。

つまり、継承時にpublic/private/protectedを指定すると、次のように振る舞います。

  1. 名前がないので、同じクラスを複数回基底クラスとして定義することはできません。
  2. public指定するとDerivedクラスの外からもアクセスできます。
  3. private指定するとDerivedクラスの外からアクセスできません。
  4. protected指定するとDerivedクラスの外からアクセスできませんが派生クラスからはアクセスできます。

このように無名のメンバ変数として考えると覚えやすいと思います。

#include <iostream>

class Base
{
public:
    int mBaseVar0;
    int mBaseVar1;
    Base() : mBaseVar0(10), mBaseVar1(11) { }
};

class PrivateDerived : private Base
{
public:
    PrivateDerived()
    {
        std::cout << "mBaseVar0=" << mBaseVar0 << "\n"; // アクセス可能
    }
};

class DerivedPrivateDerived : public PrivateDerived
{
public:
    DerivedPrivateDerived()
    {
//      std::cout << "mBaseVar0=" << mBaseVar0 << "\n"; // アクセス不可
    }
};

int main()
{
    PrivateDerived aPrivateDerived;
//  std::cout << "aPrivateDerived.mBaseVar0=" << aPrivateDerived.mBaseVar0 << "\n"; // アクセス不可
}

Wandboxで試してみる

#include <iostream>

class Base
{
public:
    int mBaseVar0;
    int mBaseVar1;
    Base() : mBaseVar0(10), mBaseVar1(11) { }
};

class ProtectedDerived : protected Base
{
public:
    ProtectedDerived()
    {
        std::cout << "mBaseVar0=" << mBaseVar0 << "\n"; // アクセス可能
    }
};

class DerivedProtectedDerived : public ProtectedDerived
{
public:
    DerivedProtectedDerived()
    {
        std::cout << "mBaseVar0=" << mBaseVar0 << "\n"; // アクセス可能
    }
};

int main()
{
    ProtectedDerived aProtectedDerived;
//  std::cout << "aProtectedDerived.mBaseVar0=" << aProtectedDerived.mBaseVar0 << "\n"; // アクセス不可
}

Wandboxで試してみる
 

2-2.ポインタの型変換

また、前回派生クラス変数へのポインタを基底クラスへのポインタへ変換できる旨を説明しましたが、これはpublic指定して継承しているので、外部からも基底クラスが見えるため変換できます。

privateやprotected指定した時は、クラス外部から基底クラスが見えませんので変換できません。

int main()
{
    Derived aDerived ;
    Base* base_ptr0=&aDerived;
    std::cout << "base_ptr0->mBaseVar0   = " << base_ptr0->mBaseVar0 << "\n";

    PrivateDerived aPrivateDerived;
//  Base* base_ptr1=&aPrivateDerived;    // エラー
//  std::cout << "base_ptr1->mBaseVar0   = " << base_ptr1->mBaseVar0 << "\n";

    ProtectedDerived aProtectedDerived;
//  Base* base_ptr2=&aProtectedDerived;  // エラー
//  std::cout << "base_ptr2->mBaseVar0   = " << base_ptr2->mBaseVar0 << "\n";
}

Wandboxで試してみる
 

2-3.private継承はどんな時使うのか?

前回のusingと組み合わせることで、private継承したクラスの一部の機能をpublicにすることができます。

#include <iostream>

class Base
{
public:
    int mBaseVar0;
    int mBaseVar1;
    Base() : mBaseVar0(10), mBaseVar1(11) { }
};

class PrivateDerived : private Base
{
public:
    PrivateDerived()
    {
        std::cout << "mBaseVar0=" << mBaseVar0 << "\n"; // アクセス可能
    }
    using Base::mBaseVar0; // mBaseVer0をpublicにする
};

int main()
{
    PrivateDerived aPrivateDerived;
    std::cout << "aPrivateDerived.mBaseVar0=" << aPrivateDerived.mBaseVar0 << "\n"; // アクセス可
}

Wandboxで試してみる

例えば、前回、std::stringをpublic継承してStringSplitを作りました。その中で注意事項に書いたようにStringSplitのデストラクタでリソースやメモリを開放していると危険です。(この注意事項は先程追加しました。まだ見てない方は是非ご一読下さい。)

std::string* p=StringSplit;
delete p;    // StringSplitのデストラクタが呼ばれないのでリソース/メモリ・リークする

これを防ぐため、StringSplitのデストラクタでリソースやメモリを開放したい場合、public継承ではなくprivate継承することが考えられます。そうしておくとstd::string* p=StringSplit;がコンパイル・エラーになるため、間違ってStringSplitのデストラクタが呼ばれなくなる心配がなくなります。

前回のSplitStringをprivate継承で実装してみました。
private版SplitString
#ifndef STRING_SPLIT_H
#define STRING_SPLIT_H

#include <iostream>
#include <string>

class StringSplit : private std::string
{
public:

//      ---<<< std::string >>>---

    using std::string::string;
    using std::string::operator=;

    using std::string::begin;
    using std::string::end;
    using std::string::rbegin;
    using std::string::rend;
    using std::string::cbegin;
    using std::string::cend;
    using std::string::crbegin;
    using std::string::crend;

    using std::string::size;
    using std::string::length;
    using std::string::max_size;
    using std::string::resize;
    using std::string::capacity;
    using std::string::reserve;
    using std::string::clear;
    using std::string::empty;
    using std::string::shrink_to_fit;

    using std::string::operator[];
    using std::string::at;
    using std::string::back;
    using std::string::front;

    using std::string::operator+=;
    using std::string::append;
    using std::string::push_back;
    using std::string::assign;
    using std::string::insert;
    using std::string::erase;
    using std::string::replace;
    using std::string::swap;
    using std::string::pop_back;

    using std::string::c_str;
    using std::string::data;
    using std::string::get_allocator;
    using std::string::copy;
    using std::string::find;
    using std::string::rfind;
    using std::string::find_first_of;
    using std::string::find_last_of;
    using std::string::find_first_not_of;
    using std::string::find_last_not_of;
    using std::string::substr;
    using std::string::compare;

    using std::string::npos;

//      ---<<< additional functions >>>---

    StringSplit split(char iSeparator)
    {
        StringSplit ret;
        std::size_t pos=find(iSeparator);
        if (pos == std::string::npos)
        {
            ret=std::move(*this);
    return ret;
        }

        ret=substr(0, pos);
        *this=substr(pos+1);
        return ret;
    }

    operator bool() const
    {
        return !empty();
    }

    std::string&       str()       { return *this; }
    std::string const& str() const { return *this; }

//      ---<<< friend functions >>>---

    friend StringSplit operator+(const StringSplit& lhs, const StringSplit& rhs) { return std::operator+(lhs.str(), rhs.str()).c_str(); }
    friend StringSplit operator+(StringSplit&&      lhs, StringSplit&&      rhs) { return std::operator+(lhs.str(), rhs.str()).c_str(); }
    friend StringSplit operator+(StringSplit&&      lhs, const StringSplit& rhs) { return std::operator+(lhs.str(), rhs.str()).c_str(); }
    friend StringSplit operator+(const StringSplit& lhs, StringSplit&&      rhs) { return std::operator+(lhs.str(), rhs.str()).c_str(); }
    friend StringSplit operator+(const StringSplit& lhs, const char*        rhs) { return std::operator+(lhs.str(), rhs).      c_str(); }
    friend StringSplit operator+(StringSplit&&      lhs, const char*        rhs) { return std::operator+(lhs.str(), rhs).      c_str(); }
    friend StringSplit operator+(const char*        lhs, const StringSplit& rhs) { return std::operator+(lhs,       rhs.str()).c_str(); }
    friend StringSplit operator+(const char*        lhs, StringSplit&&      rhs) { return std::operator+(lhs,       rhs.str()).c_str(); }
    friend StringSplit operator+(const StringSplit& lhs, char               rhs) { return std::operator+(lhs.str(), rhs).      c_str(); }
    friend StringSplit operator+(StringSplit&&      lhs, char               rhs) { return std::operator+(lhs.str(), rhs).      c_str(); }
    friend StringSplit operator+(char               lhs, const StringSplit& rhs) { return std::operator+(lhs,       rhs.str()).c_str(); }
    friend StringSplit operator+(char               lhs, StringSplit&&      rhs) { return std::operator+(lhs,       rhs.str()).c_str(); }

    friend bool operator==(const StringSplit& lhs, const StringSplit& rhs) { return std::operator==(lhs.str(), rhs.str()); }
    friend bool operator==(const char*        lhs, const StringSplit& rhs) { return std::operator==(lhs,       rhs.str()); }
    friend bool operator==(const StringSplit& lhs, const char*        rhs) { return std::operator==(lhs.str(), rhs);       }
    friend bool operator!=(const StringSplit& lhs, const StringSplit& rhs) { return std::operator!=(lhs.str(), rhs.str()); }
    friend bool operator!=(const char*        lhs, const StringSplit& rhs) { return std::operator!=(lhs,       rhs.str()); }
    friend bool operator!=(const StringSplit& lhs, const char*        rhs) { return std::operator!=(lhs.str(), rhs);       }
    friend bool operator< (const StringSplit& lhs, const StringSplit& rhs) { return std::operator< (lhs.str(), rhs.str()); }
    friend bool operator< (const char*        lhs, const StringSplit& rhs) { return std::operator< (lhs,       rhs.str()); }
    friend bool operator< (const StringSplit& lhs, const char*        rhs) { return std::operator< (lhs.str(), rhs);       }
    friend bool operator<=(const StringSplit& lhs, const StringSplit& rhs) { return std::operator<=(lhs.str(), rhs.str()); }
    friend bool operator<=(const char*        lhs, const StringSplit& rhs) { return std::operator<=(lhs,       rhs.str()); }
    friend bool operator<=(const StringSplit& lhs, const char*        rhs) { return std::operator<=(lhs.str(), rhs);       }
    friend bool operator> (const StringSplit& lhs, const StringSplit& rhs) { return std::operator> (lhs.str(), rhs.str()); }
    friend bool operator> (const char*        lhs, const StringSplit& rhs) { return std::operator> (lhs,       rhs.str()); }
    friend bool operator> (const StringSplit& lhs, const char*        rhs) { return std::operator> (lhs.str(), rhs);       }
    friend bool operator>=(const StringSplit& lhs, const StringSplit& rhs) { return std::operator>=(lhs.str(), rhs.str()); }
    friend bool operator>=(const char*        lhs, const StringSplit& rhs) { return std::operator>=(lhs,       rhs.str()); }
    friend bool operator>=(const StringSplit& lhs, const char*        rhs) { return std::operator>=(lhs.str(), rhs);       }

    friend void swap(StringSplit& x, StringSplit& y) { std::swap(x.str(), y.str()); }

    friend std::istream& operator>>(std::istream& is, StringSplit& str)       { is >> str.str(); return is; }
    friend std::ostream& operator<<(std::ostream& os, const StringSplit& str) { os << str.str(); return os; }

    friend std::istream& getline(std::istream&  is, StringSplit& str, char delim) { return std::getline(is, str.str(), delim); }
    friend std::istream& getline(std::istream&& is, StringSplit& str, char delim) { return std::getline(is, str.str(), delim); }
    friend std::istream& getline(std::istream&  is, StringSplit& str)             { return std::getline(is, str.str()); }
    friend std::istream& getline(std::istream&& is, StringSplit& str)             { return std::getline(is, str.str()); }
};

#endif  // STRING_SPLIT_H
#include <iostream>
#include <sstream>
#include "split.h"

int main()
{
    StringSplit aString="abc,def,ghi";
    aString += ",jkl";
    while(StringSplit str=aString.split(','))
    {
        std::cout << str << " : " << aString << "\n";
    }

    aString="abc";
    StringSplit aString1=aString + " + " + StringSplit("def");
    std::cout << aString1 << "\n";

    if (aString == aString1) std::cout << "match0\n";
    if (aString == aString ) std::cout << "match1\n";

    std::cout << "aString=[" << aString << "] aString1=[" << aString1 << "]\n";
    swap(aString, aString1);
    std::cout << "aString=[" << aString << "] aString1=[" << aString1 << "]\n";

    std::stringstream ss("uvw xyz\n");
    ss >> aString;
    getline(ss, aString1);
    std::cout << "aString=[" << aString << "] aString1=[" << aString1 << "]\n";
}
project(split)

if(MSVC)
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4 /EHsc")
else()
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -std=c++11")
endif()

add_executable(split split.cpp split.h)

Wandboxで試してみる

メンバ関数はusingで取り込めるので楽なのですが、std::stringを引数とするグローバル関数が標準で定義されています。
それらは1つ1つ中継する必要があるため、手間が掛かります。ここに記載されているNon-member function overloadsに対応してみましたが、他にもstd::stoiなど多数の標準関数があるので全部に対応するのはちょっと厳いです。サンプルは継承したstd::string()型変数を返却するstr()メンバ関数を提供し、StringSplitを使いたい時はstr()メンバ関数を使ってstd::string型としてアクセスする方法も考えられます。

 

2-4.protected継承はどんな時使うのか?

身もふたもないのですが、protected継承を使う場面は良く分かりません。私は使ったことありません。必要になるケースはかなり少ないと思います。
 

2-5.publicとprivateを省略したらどうなる?

第20回目に説明したのと同じく、structとclassのデフォルトと同じです。public/private/protectedを省略するとclassならprivate継承、structならpublic継承となります。(ここも無名変数を定義したのと同じ振る舞いですね。)

#include <iostream>

class Base
{
public:
    int mBaseVar0;
    int mBaseVar1;
    Base() : mBaseVar0(10), mBaseVar1(11) { }
};

struct Derived : Base
{
    Derived() { }
};

class PrivateDerived : Base
{
public:
    PrivateDerived() { }
};

int main()
{
    Derived aDerived;
    std::cout << "aDerived.mBaseVar0=" << aDerived.mBaseVar0 << "\n";

    PrivateDerived aPrivateDerived;
//  std::cout << "aPrivateDerived.mBaseVar0=" << aPrivateDerived.mBaseVar0 << "\n";  // アクセス不可
}

Wandboxで試してみる

 

3.まとめ

C++とは関係ない話なのですが、実は先日、Theoride Technologyのロゴをクラウド・ワークスで募集して作って頂きました。
最初はフォントの加工ツールを使って自力で作ったのですが、やはりイマサンでした。でも、製作会社に頼むとかなり高価なので躊躇してました。しかし、クラウド・ソーシングで依頼すると製作会社へ依頼するよりリーズナブルな価格で、かつ、多くのデザイナーの方にご提案頂けました。結果、良いロゴを作れました。これは良いサービスです。

今回は「継承」の応用的な部分を解説しましたが、もう少し「継承」で解説しておくべきことが残ってしまいました。
恐らく次回で「継承」の解説を終了し、その次からはいよいよ「ポリモーフィズム」を解説できると思います。
お楽しみに。