2
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Javaのサンプルでよく見るインターフェースクラス実装の利点をC++に実現してみた。

Last updated at Posted at 2017-05-06

2017/5/8修正: newで確保したメモリをdeleteさせるように。
2020/10/16修正: 静的ポリモーフィズムを追記。

インターフェースってC++にあるっけか?

 C++はとても奥が深い言語なので、マネージ系にありふれているインターフェースみたいな機能もやろうと思えばできる。
 だがそれはそれで問題を抱えているため、C++のインターフェースは別のアプローチで実現される。

とりまマネージ系っぽいインターフェースを実装してみる。

 C++は標準の言語仕様においてインターフェースクラスを明示的に持っているわけではないが、純粋仮想関数による抽象クラスのひとつの形としてこれを実現することができる。
 ただ、インターフェースクラス自体、実体のないクラスなのでこれを用いる意味があるのかという問いがあり、Javaにおいてこれを調べると「インターフェースクラスだとスマートに表現できる」と実例が載っており、実際「インターフェースクラス 利点」でぐぐればそういった用例がトップのほうに上がってくる。
 この「インターフェースクラスの利点」の理屈自体はGoogle先生に頼って見てもらうとして、これをC++でやったらどうなるんだろうと試してみたらどうなるかは、以下の結果とする。

istest.cpp
# include <iostream>
# include <string>

//標準C++におけるインターフェース実装のテスト

using namespace std;

//インターフェースクラス部分。
//C++としては純粋仮想関数を含む抽象クラスとしての形になる。
//メンバ関数に入れる二つの文字列に対する処理は実装するクラスに投げ込む。
class ITwinString{
    private:
    public:
    virtual ~ITwinString() {}
    virtual wstring StringProcess(wstring StringOne, wstring Stringtwo) = 0;
};

//クラスAは、二つの文字列を連結する。
class ConbString : public ITwinString {
    private:
    wstring ConbStringThis;
    public:
    virtual wstring StringProcess(wstring StringOne, wstring StringTwo)
    {
        ConbStringThis = StringOne + StringTwo;
        return ConbStringThis;
    }
};

//クラスBは、二つの文字列を比較して同じならTRUE、違うならFALSEを返す。
class CompareString : public ITwinString {
    private:
    wstring CompareStringThis;
    public:
    virtual wstring StringProcess(wstring StringOne, wstring StringTwo)
    {
        if (StringOne == StringTwo)
        {
            CompareStringThis = L"TRUE THIS";
        }
        else
        {
            CompareStringThis = L"FALSE THIS";
        }
        return CompareStringThis;
    }
};

//上の実装クラスを受け取って処理するクラス。
class StringOut
{
    private:
    wstring StrOne = L"hogehoge";
    wstring StrTwo = L"hogehoge";

    public:
    void OutString(ITwinString* IS)
    {
        wcout << IS->StringProcess(StrOne, StrTwo) << "\n";
    }
};

//main関数。
int main()
{
    StringOut* OS = new StringOut;
    OS->OutString(new ConbString);
    OS->OutString(new CompareString);
}
実行結果
hogehogehogehoge
TRUE THIS

 処理メンバ関数にあるインターフェースクラスの仮引数部分に、実装クラスのインスタンスを投げ込むことでインターフェースクラス記述がスマートになるという例のC++版になる。
 C++で注意するのは、インターフェースクラスの仮引数をポインタ指定しないと抽象クラスのインスタンス作成エラーが返ってくること。インターフェースの継承のアクセス権を閉じないことである。

 この挙動は「動的ポリモーフィズム」といい、純粋仮想関数による関数テーブルによって管理されたポインタを使用して実現している。
 だがこの動作はスタックの圧迫、関数呼び出しのオーバーヘッドの増加、インライン展開が不可能といったパフォーマンスへの悪影響を生み出すため、C++において動的ポリモーフィズムはあまり好まれない実装と言われている。

動的があるなら静的がある世界

 ではどうすればいいかといえば、「動的(Dynamic)」があるなら「静的(static)」なポリモーフィズムがあるので、それで実現すればいい。
 C++はテンプレートを用いることでコンパイル時に解決する機能を備えており、これによってポリモーフィズムの動作をコンパイル時に決定してしまうことでパフォーマンスへの影響を抑えることができる。

 上記の動的ポリモーフィズムと同等の機能を実現する、実際のコードは以下。
 若干APIが変更されているのは記事更新までの期間の長さによるものと、参考元による部分のためご容赦願いたい。

istmp.cpp
# include <iostream>
# include <string>
# include <memory>
# include <type_traits>

//prototype
template <class T>
class IMakeString;

//Printer template
template <class T, bool isExtended = std::is_base_of<IMakeString<T>, T>::value>
class Printer {
    static_assert(isExtended, "T is not extended interface class.");
};

template <class T>
class Printer<T, true> {
    private:
        std::string printMessage;

    public:
        Printer(std::string, std::string);
        void getMessage();
};

template <class T>
Printer<T, true>::Printer(std::string strone, std::string strtwo) {
    auto PrintClass = std::make_shared<T>();
    printMessage = PrintClass->stringProcess(strone, strtwo);
}

template <class T>
void Printer<T, true>::getMessage() {
   std::cout << printMessage << std::endl;
}

//Base Class
template <class T>
class IMakeString {
    private:
    public:
        std::string stringProcess(std::string one, std::string two) {
            static_cast<T &>(this)->stringProcess(one, two);
        }
};

class ConbString : public IMakeString<ConbString> {
    private:
        std::string conbineStringThis;
    public:
        auto stringProcess(std::string one, std::string two) {
            conbineStringThis = one + two;
            return conbineStringThis;
        }
};

class CompString : public IMakeString<CompString> {
    private:
        std::string compareStringThis;
    public:
        auto stringProcess(std::string one, std::string two) {
            if (one == two) {
                compareStringThis = "true this";
            } else {
                compareStringThis = "false this";
            }
            return compareStringThis;
        }
};

int main() {
    Printer<ConbString> str1("Hello", "World");
    Printer<CompString> str2("Foo", "Bar");
 
    str1.getMessage();
    str2.getMessage();
}

改善点

  • テンプレートを利用して静的ポリモーフィズムを実現する。
  • myコード規約を厳密に適用してコードの体裁を一定にする。
  • 実際の出力を行うクラスのPrinterはテンプレートメタプログラミングを拝借して、間違ったテンプレート引数を排除する。
  • テンプレート引数にstatic_castを使うことで、インターフェースとして使用したい機能をthisポインタに割り当てて呼び出すことで、純粋仮想関数を使う代わりにしている。
  • 生ポインタのnew生成からスマートポインタを使用するよう変更し、main関数ではなくPrinterのコンストラクタ内でコントロールするように。デストラクタ+deleterは明示しなくてもたぶん大丈夫だと思うが必要なら追記する。
  • スマートポインタ作成と派生クラスのstringProcessメソッドをautoで推論させる。

参考元

まとめ

 最新更新時の地点で何かもっとC++の知見を深めようと思っていろいろやっていた結果、昔にアウトプットしたC++の記事を更新したいと思い立って記述した。
 C++も日進月歩で新機能を実装して時代に食いついているのだから、日々学ばない手はないし、今後C++でプロジェクトを書く場合に十分な基礎を身に着けておかなければと思い立つ次第である。

2
5
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?