C++のインターフェース

アバター
GRAM
記事: 164
登録日時: 14年前
住所: 大阪

C++のインターフェース

投稿記事 by GRAM » 14年前

インターフェースというものがあるらしい。というかあるわけですが。

いろいろなところであたかも簡単で特に難しいことがないように書かれているのを見るんですが、(というか自分もそう思ってたんですが)
実は難しいんじゃないか疑惑が自分の中で起こったのでメモしておきます。

まずインターフェースクラスですが、基本的にメンバ関数がすべて純粋仮想関数になっていてメンバ変数を持たないようなやつのことを言うようです

CODE:

//---------IHogeBase.h----------

#pragma once
#include 
#include 

class IHogeBase{
public:
	virtual ~IHogeBase(){};
	virtual void PrintName() const = 0;
	virtual void ResetName(const std::string& name) = 0;
protected:
	IHogeBase(){}
	IHogeBase(const IHogeBase&){}
	IHogeBase& operator= (const IHogeBase&){}
};

std::shared_ptr CreateHogeBase(const std::string& name);
で、こいつの実装はほかのヘッダとCppで行うと

CODE:

// -------CHogeBase.h-------------

#pragma once
#include "IHogeBase.h"

class CHogeBase:public IHogeBase{
public:
	CHogeBase(const std::string& name):m_Name(name){}

	void PrintName() const;
	void ResetName(const std::string& name){
		m_Name = name;
	}
protected:
	const std::string& GetName() const{
		return m_Name;
	}
private:
	std::string m_Name;
};
と生成関数とプリントアウト関数の定義ファイル

CODE:

//-----CHogeBase.cpp--------

#include "CHogeBase.h"
#include 

void CHogeBase::PrintName() const{
	std::cout  CreateHogeBase(const std::string& name){
	return std::shared_ptr(new CHogeBase(name));
}
・・・ここまではわかります。こうすることでインターフェースを使う側からは(CHogeBase.hでも読み込まない限り)実装に関する情報が全く見えなくなると。


で、問題はIHogeBaseを継承してIHogeDerivedを作るにはどうしたらいいか?という話に収束します。
ここで若干わからなくなりました。
まずインターフェース自体の継承は簡単です

CODE:

//-------IHogeDerived.h----------

#pragma once
#include "IHogeBase.h"

class IHogeDerived:public IHogeBase{
public:
	enum Type{
		One = 1,
		Two,
		Three
	};
public:
	virtual ~IHogeDerived(){};
	virtual void PrintName() const = 0;
	virtual void ResetName(const std::string&) = 0;
	virtual void PrintType() const = 0;
protected:
	IHogeDerived(){}
	IHogeDerived(const IHogeDerived&){}
	IHogeDerived& operator=(const IHogeDerived&){}
};

std::shared_ptr CreateHogeDerived(const std::string& name, IHogeDerived::Type);
特に面白味もないですが、enumの列挙子を格納してPrintTypeでそれを表示するという関数を追加しました
はい。

で、ここからなんですが、実装どうしたらいいんでしょう?

ええ、簡単な解決策はあります。
IHogeDerivedを継承してCHogeDerivedを作り、純粋仮想関数のすべてをオーバーライドして関数を定義すると。
でもはっきり言ってPrintNameの実装なんてのはCHogeBaseのものと同じで構わないわけです。

はーい、きました。コードの重複。
残念ながらIHogeDerivedを継承しただけではどう頑張ってもCHogeBaseのメンバは呼べないわけです。

どうしましょう?どうにも自分にはよくわかりません。何が正解なんでしょうね?
自分で考えた苦肉の策は、コンポジットパターンです

CODE:

//-------CHogeDerived.h--------

#pragma once
#include "CHogeBase.h"
#include "IHogeDerived.h"

class CHogeDerived:
	public IHogeDerived,private CHogeBase{
public:
	CHogeDerived(const std::string& name, IHogeDerived::Type type):CHogeBase(name),m_Type(type){
	}
	void PrintName() const{
		CHogeBase::PrintName();
	}
	void ResetName(const std::string& name){
		CHogeBase::ResetName(name);
	}
	void PrintType() const;
private:
	IHogeDerived::Type m_Type;
};
および生成関数の定義ファイル

CODE:

//--------CHogeDerived.cpp---------

#include "CHogeDerived.h"
#include 


void CHogeDerived::PrintType() const {
	std::cout  CreateHogeDerived(const std::string& name, IHogeDerived::Type type){
	return std::shared_ptr(new CHogeDerived(name, type));
}
private継承したのはprotectedな関数を呼ぶためです。
これでとりあえず目的は果たしました。

CODE:

//------main.cpp----------

#include "IHogeDerived.h"


void ChangeName(std::shared_ptr ptr, std::string& name){
	ptr->ResetName(name);
	ptr->PrintName();
}

int main(){
	std::shared_ptr base = CreateHogeBase(std::string("Base"));
	std::shared_ptr derived = CreateHogeDerived(std::string("Derived"), IHogeDerived::One);

	base->PrintName();
	ChangeName(base,std::string("Changed base"));


	derived->PrintName();
	ChangeName(derived, std::string("Changed derived"));
	derived->PrintType();
	return 0;
}
はい。期待したとおりに動きます。
しかし、何とも不格好な気がしてならない・・・
もっといい解決策はないんですかね?
最後に編集したユーザー GRAM on 2011年5月18日(水) 23:26 [ 編集 1 回目 ]

アバター
tk-xleader
記事: 158
登録日時: 14年前

RE: C++のインターフェース

投稿記事 by tk-xleader » 14年前

テンプレートを使って実装するインターフェイスをパラメータ化するというのはどうでしょう?

CODE:

//---------IHogeBase.h----------
 
#pragma once
#include 
#include 
 
class IHogeBase{
public:
    virtual ~IHogeBase(){};
    virtual void PrintName() const = 0;
    virtual void ResetName(const std::string& name) = 0;
protected:
    IHogeBase(){}
    IHogeBase(const IHogeBase&){}
    IHogeBase& operator= (const IHogeBase&){}
};
 
std::shared_ptr CreateHogeBase(const std::string& name);

// -------CHogeBase.h-------------
 
#pragma once
#include "IHogeBase.h"
#include 
 
template
class CHogeBaseT:public I{
	static_assert(std::is_base_of::value,"Template Parameter I must be derived from IHogeBase!!");
public:
    CHogeBaseT(const std::string& name):m_Name(name){}
 
    void PrintName() const{
    	std::cout 

typedef CHogeBaseT CHogeBase;

std::shared_ptr CreateHogeBase(const std::string& name){
    return std::shared_ptr(new CHogeBase(name));
}

//-------IHogeDerived.h----------
 
#pragma once
#include "IHogeBase.h"
 
class IHogeDerived:public IHogeBase{
public:
    enum Type{
        One = 1,
        Two,
        Three
    };
public:
    virtual ~IHogeDerived(){};
    virtual void PrintName() const = 0;
    virtual void ResetName(const std::string&) = 0;
    virtual void PrintType() const = 0;
protected:
    IHogeDerived(){}
    IHogeDerived(const IHogeDerived&){}
    IHogeDerived& operator=(const IHogeDerived&){}
};
 
std::shared_ptr CreateHogeDerived(const std::string& name, IHogeDerived::Type);

//-------CHogeDerived.h--------
 
#pragma once
#include "CHogeBase.h"
#include "IHogeDerived.h"
 
template
class CHogeDerivedT:
    public CHogeBaseT{
    static_assert(std::is_base_of::value,"Template Parameter I must be derived from IHogeDerived!!");
public:
    CHogeDerived(const std::string& name, IHogeDerived::Type type):CHogeBaseT(name),m_Type(type){
    }
    void PrintType() const{
    	std::cout 

typedef CHogeDerivedT CHogeDerived;

std::shared_ptr CreateHogeDerived(const std::string& name, IHogeDerived::Type type){
    return std::shared_ptr(new CHogeDerived(name, type));
}
でもこれはこれで不恰好ですね…

ISLe
記事: 2650
登録日時: 14年前

Re: C++のインターフェース

投稿記事 by ISLe » 14年前

CHogeBaseの実装が使えるなら、IHogeBaseでなくてCHogeBaseを継承すれば良いだけなのでは。
GRAM さんが書きました:まずインターフェースクラスですが、基本的にメンバ関数がすべて純粋仮想関数になっていてメンバ変数を持たないようなやつのことを言うようです
既にこの辺からいろいろ間違っている気がします。
Javaだとインターフェースと抽象クラスは明確に区別されているのですけど。

アバター
GRAM
記事: 164
登録日時: 14年前
住所: 大阪

RE: C++のインターフェース

投稿記事 by GRAM » 14年前

tkmakwins15 さんが書きました:テンプレートを使って実装するインターフェイスをパラメータ化するというのはどうでしょう?
・・・
えぇそんな感じの奴も考えました。
ただそれだといくつかのインターフェースを多重継承して新しくインターフェースを作るとき詰むんじゃないかなぁと思うのですが・・・
(というのもその実装は一番下の階層のテンプレートパラメーターがずっと上まで引き継がれるのですよね、間違ってたらすみません)

ISLe さんが書きました: CHogeBaseの実装が使えるなら、IHogeBaseでなくてCHogeBaseを継承すれば良いだけなのでは。

GRAM さんが書きました:
まずインターフェースクラスですが、基本的にメンバ関数がすべて純粋仮想関数になっていてメンバ変数を持たないようなやつのことを言うようです

既にこの辺からいろいろ間違っている気がします。
Javaだとインターフェースと抽象クラスは明確に区別されているのですけど。
うーん、まず一つ目です。
CHogeBaseを継承すればよいというのはIHogeDerivedででしょうか?
もしそうだとすればそれはいただけないです。
CHogeBaseの実装がIHogeDerivedに漏れちゃいます。そうするとCHogeBaseの実装をちょっとでも変更する(メンバ変数を一つ足す、プライベートなメンバ関数を一つ足す等)
だけでIHogeDerivedをインクルードする全ファイルをコンパイルしなおす必要が生じます。
インターフェースの意義が幾分損なわれる気がします

もしそうでなくてCHogeDerivedでpublic継承するという意味でしたらそれはできないと思います。
というのもIHogeDerivedはIHogeBaseを継承しており(これは絶対に必要でしょう)、結局CHogeDerivedでは全仮想関数を定義しなくてはならないからです。
(たとえCHogeBaseでそれが定義されていたとしても)
だとすると結局は実装は上記のような感じになるでしょう。CHogeBaseはCHogeDerivedの実装に使われているだけですので(CHogeDerivedからCHogeBaseへのアップキャストは行わないので)、
public継承するメリットもあまりないように思えます。

二つ目ですが、
メンバ関数がすべて純粋仮想関数になっていてメンバ変数を持たないようなやつという表現がまずかったのですかね?
要は実装に関する情報を持たないということが言いたかっただけです。
Javaでいうabstractとは違います。この場合はまさにJavaのInterface指すのだと思います
C++でいう通常の仮想関数と実装を含む抽象クラスとは先の依存性をなくせるという意味でも役割が異なると思うのですが

ISLe
記事: 2650
登録日時: 14年前

Re: C++のインターフェース

投稿記事 by ISLe » 14年前

GRAM さんが書きました:メンバ関数がすべて純粋仮想関数になっていてメンバ変数を持たないようなやつという表現がまずかったのですかね?
要は実装に関する情報を持たないということが言いたかっただけです。
Javaでいうabstractとは違います。この場合はまさにJavaのInterface指すのだと思います
そうであれば矢印の向きが逆というか、定義の導き方がおかしいと感じるのです。
インターフェースを抽象クラス的に使う方法を模索しているように見えます。
インターフェースはあくまでインターフェースなので、インターフェースを継承するときに同じ実装が使えるかどうかはどうでも良いことですよね。
極端な話、実装クラスが直接インターフェースを継承している必要もないわけですし。

アバター
GRAM
記事: 164
登録日時: 14年前
住所: 大阪

Re: C++のインターフェース

投稿記事 by GRAM » 14年前

ISLe さんが書きました:インターフェースはあくまでインターフェースなので、インターフェースを継承するときに同じ実装が使えるかどうかはどうでも良いことですよね。
極端な話、実装クラスが直接インターフェースを継承している必要もないわけですし。
そこが自分の考えと違うんだと思います。そりゃ使う側からすればどうでもよいことでしょうけれど、作る側からすればそこに使えるコードが転がってるのにそれを使わないのですか?
今の疑問点は、あくまで実装する側がどうやって使いまわせるコードを使うのか?という話です。
結論としてそれが妥当でないということになるのならばそれでもかまいませんが、それには何らかの納得いく理由が欲しいなぁと思っているわけです。

まとめると思考しているのは
・使う側からはインスタンスの振る舞い(実装)が全く見えないようなクラスをつくり、(これは別になんの疑問もなく行える)
 それらの実装部分を考えるときに簡単にコードの重複を避けるすべはあるか?
ということです。もちろんこの場合考えている重複は、インターフェース同士にパブリックな継承関係がある時です。(単純に実装クラスがインターフェースを直接継承した場合)
要は上の要件を満たせれば、それこそ極端な話でもいいですよ。(ただし簡単に避けるという要件を満たせるかはわかりませんが)

アバター
tk-xleader
記事: 158
登録日時: 14年前

RE: C++のインターフェース

投稿記事 by tk-xleader » 14年前

他には、仮想継承を使うとか…

CODE:

#include

struct ISampleBase{
	virtual void MethodFunc1(int) const = 0;
	virtual ~ISampleBase(){}
};

struct ISampleDerived : virtual ISampleBase{
	virtual void MethodFunc2(double) const = 0;
	virtual ~ISampleDerived(){}
};

class CSampleBase : public virtual ISampleBase{
	int mValue1;
public:
	CSampleBase(int value):mValue1(value){}
	virtual void MethodFunc1(int value) const{
		std::coutMethodFunc1(50);
	pSample2->MethodFunc1(100);
	pSample2->MethodFunc2(1.0);
	delete pSample2;
	delete pSample1;
}
しかし、インターフェイスに対して仮想継承というのもあまり好きなスタイルではないですね…

ISLe
記事: 2650
登録日時: 14年前

Re: C++のインターフェース

投稿記事 by ISLe » 14年前

GRAM さんが書きました:今の疑問点は、あくまで実装する側がどうやって使いまわせるコードを使うのか?という話です。
結論としてそれが妥当でないということになるのならばそれでもかまいませんが、それには何らかの納得いく理由が欲しいなぁと思っているわけです。
余計な口出ししてすみませんでした。頑張ってください。