C++でのポリモーフィズムについて

フォーラム(掲示板)ルール
フォーラム(掲示板)ルールはこちら  ※コードを貼り付ける場合は [code][/code] で囲って下さい。詳しくはこちら
chibago

C++でのポリモーフィズムについて

#1

投稿記事 by chibago » 13年前

皆様、お世話になっております。
表題どおりですが、本件に関してずっと解決策を考えておりました。

先ほど、良くまとめられているホームページを見つけました。
http://d.hatena.ne.jp/negation/20111203/1322876171

丁度、私がやろうとしているものも、同類の複数クラスのインスタンスを
自作のコンテナクラスに登録して、必要に応じて呼び出そうとするものです。

結局のところ、この方は
C++でポリモーフィズムを用いるべきではないと
結論づけているようです。

ただ、この手の実装は使わざるを得ないので何とかしたいと考えております。
(PYTHONで技巧的なものを作った経験がありますので、
これがないと不便です。)

個人的には、紹介されている中では
BOOSTのアップキャストされないshared_ptrと上位クラスに
ダミーの実装をすべて書き込むと言う案が一番良さそうかと思いました。

本当に、C++でポリモーフィズムを効率的に実装する方法はないのでしょうか。
(C++がオブジェクト指向言語の代名詞のようになっているので、
この事実は少々ショックでした。)

ご意見をお聞かせ下さい。

YuO
記事: 947
登録日時: 15年前
住所: 東京都世田谷区

Re: C++でのポリモーフィズムについて

#2

投稿記事 by YuO » 13年前

何をもって効率的と定義しているのでしょうか。

C++ は静的型付け言語です。
その前提下において,「統一的な外部インターフェース」を持つクラス群を扱う機構としてC++の仮想関数を使った多態があります。
これを逸脱した物が使いにくいのは当然です。

C++ として普通に使う分に,多態性が使いにくいことはないです。
C++ を他の言語と同じように使おうとするならば,使いにくいこともあると思います。
単純に,言語にあわせて設計をすればいいだけの話ですよ。

chibago

Re: C++でのポリモーフィズムについて

#3

投稿記事 by chibago » 13年前

YuO 様、
コメントありがとうございます。

あまり、言語の位置づけまでは理解できていませんので、
的外れな発言かもしれませんが、私の効率的と言う言葉
を用いて考えているのは、引用したホームページで議論されて
いる内容とほとんど同じです。

具体的には、継承の用い方の代表としては、共通部分を基底クラス
に抽出し、変化分のみを派生クラスに記述するものと思うのですが、
私の理解する範囲では、継承を用いた多態性を実現するためには、
用いられるだろうすべての関数名をあらかじめ基底クラスに列挙
しなければならないだろうと思われます。(YuO 様の
「C++の仮想関数を使った多態」と同じでしょうか)

この方法では、本来の用途と真逆の記載が求められますし、
派生クラスを拡張した際にはそれに合わせて、基底クラスを
修正しなければなりません。引用したホームページの著者は
この点に関して効率的ではない(保守性に劣る)と指摘しています。

素人目から見ると(勘違いでしたらすみません)BOOSTのスマート
ポインタを用いればアップキャストされないと言うことは、
目標の半分程度達成されている様に見えます。(これで、
派生クラスのみに拡張されインターフェースへのアクセスが
できれば完璧です。)

YuO 様のおっしゃるように「静的型付け言語の範囲外」として
諦めればよいのか、何かやりようがあるのかを知りたくて質問
をさせていただいております。(将来、スマートポインタが拡張
されてこの問題が解決されるのが一番よいのですが、
それは望めないでしょうか。)

ご意見をお聞かせいただければ幸いです。

beatle
記事: 1281
登録日時: 14年前
住所: 埼玉
連絡を取る:

Re: C++でのポリモーフィズムについて

#4

投稿記事 by beatle » 13年前

chibago さんが書きました:私の理解する範囲では、継承を用いた多態性を実現するためには、
用いられるだろうすべての関数名をあらかじめ基底クラスに列挙
しなければならないだろうと思われます。
まさにそうなんですよね.オブジェクト指向でソフトウェアシステムを組むには,初めから十分なクラス階層の設計が求められるんです.
基底クラス型のポインタのまま呼び出したい関数は,全部基底クラスに用意しておかないといけません.一度クラス階層を作ってから,基底クラスに追加すべき関数が新たに出てくるとそれはもう大変です.
でもこの制限は,「is a」関係を満たすために重要なことなので,我慢するしかありません.

chibagoさんが示されたブログ記事にはBook, Novel, Comicクラスが出てきます.Novel, ComicはBookを継承していますから,「Novel is a Book」と「Comic is a Book」が成り立たねばいけません.現実世界では「小説は本」であり,「漫画は本」ですから,is a関係はバッチリ成り立っています.
「A is a B」関係がある場合,Bで成り立つことはすべてAでも成り立ちます.すべての本には作者名とタイトルがあるとすれば,すべての小説にも作者名とタイトルがあります.すべての漫画にも作者名とタイトルがあります.
しかしその逆は必ずしも成り立つとは限りません.例えばすべての漫画にコマ割りがあるとしても,すべての本にコマ割りがあるわけではありません.

ブログ記事ではstd::vector<Book*>という配列に全てのインスタンスを格納しています(ココではあえてshared_ptrではなく普通のポインタにしています.簡潔に書くためです.実践ではshared_ptrを使いましょう).
この配列は本棚を表しているそうです.本棚にはどんな本でも置けます.
問題になっているのは,本棚から本を順番に取り出して何か処理をするのですが,その際にBook型で扱おうとするとComic特有の操作(Comic::comic_method())を呼び出すことができなくて困った,という話です.
しかしこれは当然なのです.すべての本をBookというインターフェースを通して操作するのですから,漫画特有の事柄は見えないのです.というより,見えてはいけないのです.ポリモーフィズムのいいところは,「Bookを扱う関数」にNovelでもComicでも渡せる,という点です.Bookに対して処理を記述しておけば,NovelでもComicでも,とにかく本であればなんでも処理できます.

しかし,そうは言ってもBook型のポインタを扱いつつも,NovelやComic特有の処理もしたいことがあります.そういう場合は,取り出した本の種類を調べた上で,NovelやComicにキャストして処理する必要があります.本の種類によってあったりなかったりする処理をするわけですから,キャストしなきゃいけないのは当たり前です.

今回の疑問は,継承が持つ2つの役割を混同しているために発生している気がします.それは,is a関係を有効に利用するための継承か,単にコンテナに格納するためだけの継承か,ということです.今回のBook, Novel, Comicの例は後者です.初めはNovel専用の本棚だったけど,Comicも本棚に置きたいという要望からBookを作りました.小説と漫画を「本」として扱う処理を書きたかったわけではありません.
この例の場合,Bookの代わりにJavaでいうObject型を使っても良かったのです.本棚に「何か」を格納できれば,それでよかった.でも,それをBookという名前にしたがために,BookのままでNovelもComicも処理したいという欲望が生まれてきて,それが付け焼刃的なクラス設計を助長してしまいました.std::vector<Object*>なら,だれも「統一して処理したい」とは思わなかったでしょう.

長文になってしまいました.まとめるとこういうことです(僕のオブジェクト指向感ですから絶対正しいわけではないと思います).
もし,派生クラスに新しく関数を追加する必要が生じたら・・・
  • その関数が基底クラスにも追加するのが意味的に正しいなら,自分のクラス設計が悪かったことを悔やんで,頑張って追加しましょう.
  • その関数は,意味的には派生クラス固有のものなら基底クラスには追加せず,実際に使うときに派生クラスにキャストして使いましょう.

chibago

Re: C++でのポリモーフィズムについて

#5

投稿記事 by chibago » 13年前

beatle様、
お返事ありがとうございました。

結局それしかないのかと言うことが分かり
今後迷いなくプログラミングができます。

ただ、個人的な好みかと思いますが、
ダウンキャストだけはやりたくないなと感じております。

今後ともよろしくお願いします。

beatle
記事: 1281
登録日時: 14年前
住所: 埼玉
連絡を取る:

Re: C++でのポリモーフィズムについて

#6

投稿記事 by beatle » 13年前

ダウンキャストはなぜやりたくないのか,よろしければお聞かせ下さい.
安全性の問題でしょうか?そうだとすれば,それはすぐに解決する問題です.
速度の問題でしょうか?それならば納得です.

たかぎ
記事: 328
登録日時: 15年前
住所: 大阪
連絡を取る:

Re: C++でのポリモーフィズムについて

#7

投稿記事 by たかぎ » 13年前

単に、ダウンキャストが悪だということで避けたいのであれば、いっそのことboost::anyを使うというのも手です。
これなら基底クラスを定義しなくても、ほぼどんなクラスのデータでもコンテナに入れることができます。
boost::any_castで元の型としての値を取り出すことができますが、これはダウンキャストではありません。
(実質的には大差ありませんが...)

chibago

Re: C++でのポリモーフィズムについて

#8

投稿記事 by chibago » 13年前

beatle様、たかぎ 様、
私が、ダウンキャストを嫌う理由としては、
結局、型名を指定しないと処理できないと言うところです。

beatle様がおっしゃるところの「一緒に処理をしたいという欲望」
が満たせないというところにあります。

以前は良くPythonを便利に使っていて、重い処理が続くと
結局すべてC++で書いた方がよいと思う様になりました。

そのため、この様な思考にとらわれているのだと思います。

緩くて早い言語があればよいのですが。

たかぎ
記事: 328
登録日時: 15年前
住所: 大阪
連絡を取る:

Re: C++でのポリモーフィズムについて

#9

投稿記事 by たかぎ » 13年前

結局、制約を緩くしようとすると重くならざるを得ないので、トレードオフでしょうね。

Poco
記事: 161
登録日時: 15年前

Re: C++でのポリモーフィズムについて

#10

投稿記事 by Poco » 13年前

どうも腑に落ちないので質問させてください。

chibagoさんが最初に提示されたリンク先にC++における多態の問題を書いているのですが、
これはC++の問題ですか?他の言語だとすんなり解決できるのですか?
どうも設計が駄目なだけのような気がしてなりません。
特にnovel_method()のくだりはこんな処理系あったら良いなと思う反面、現実的にありえない気がしてならないのですが。

beatle
記事: 1281
登録日時: 14年前
住所: 埼玉
連絡を取る:

Re: C++でのポリモーフィズムについて

#11

投稿記事 by beatle » 13年前

Pythonだとどうやらchibagoさんが思うようなことが出来るらしいですので,一度Pythonで書いた例を示していただければ,「それならC++でこうやって書けるよ」みたいなアドバイスが出来るかと思います.

たかぎ
記事: 328
登録日時: 15年前
住所: 大阪
連絡を取る:

Re: C++でのポリモーフィズムについて

#12

投稿記事 by たかぎ » 13年前

Pythonは詳しくないので外しているかもしれませんが...
もしかすると、Objective-C++なら希望の機能が実現できるかもしれません。
ただし、重くなることや型安全が損なわれることは避けられません。

アバター
tk-xleader
記事: 158
登録日時: 14年前
連絡を取る:

Re: C++でのポリモーフィズムについて

#13

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

chibago さんが書きました:beatle様、たかぎ 様、
私が、ダウンキャストを嫌う理由としては、
結局、型名を指定しないと処理できないと言うところです。
では、たとえば、Comicクラスのメソッドの内部でBookの参照をComicにダウンキャストするような、「受け取ったオブジェクトを自分自身のクラスにダウンキャストする」というのならダウンキャストしてもかまわないでしょうか?

要するにこういうことです。(ちょっと微妙にずれていますけど本質的には一緒です。)

コード:

template<typename T>
struct IComparable{
	virtual bool Equals(const T& other)=0;
};
class Book : public IComparable<Book>{
	/*…色々略…*/
public:
	virtual bool Equals(Book& other){
		return other.name == this.name && other.author == this.author;
	}
};
bool operator==(const Book& book1,const Book& book2){
	return book1.Equals(book2);
}
bool operator!=(const Book& book1,const Book& book2){
	return !(book1 == book2);
}

class Comic : public Book,public IComparable<Comic>{
	/*………*/
public:
	bool Equals(const Book& other){
		const Comic *ptr = dynamic_cast<IComparable<Comic>*>(&other); //ここで、型名Comicを用いてダウンキャスト。
		if(ptr)return ptr->Equals(this);
		return Book::Equals(other);
	}
	bool Equals(const Comic& other){
		/*………*/
	}
};
<訂正>
コードが長いと思ってスポイルにしましたけれどもやはり無駄だと判断しましたから外します。

chibago

Re: C++でのポリモーフィズムについて

#14

投稿記事 by chibago » 13年前

tkmakwins15 様、
サンプルのご提示ありがとうございます。

ただ、私にはちょっと難しすぎて、ブラックボックスとしての効用すら
分からない状態です。

ところで、PYTHONではどうかという件ですが、
基本的に型と言う概念がないので何でもありと言う感じです。

Book.py

コード:

"""
Book
"""

class Book:

    def __init__(self, title, author):
        self.set_title(title)
        self.set_author(author)

    def set_title(self, val):
        self._title = val

    def set_author(self, val):
        self._author = val

    def get_title(self):
        return self._title

    def get_author(self):
        return self._author
Commic.py

コード:

"""
Commic
"""
from Book import Book

class Commic(Book):

    def __init__(self, title, author, magazin):
        Book.__init__(self, title, author)
        self.set_magazin(magazin)

    def set_magazin(self, val):
        self._magazin = val

    def get_magazin(self):
        return self._magazin
Novel.py

コード:

"""
Novel
"""
from Book import Book

class Novel(Book):

    def __init__(self, title, author, illustrator):
        Book.__init__(self, title, author)
        self.set_illustrator(illustrator)

    def set_illustrator(self, val):
        self._illustrator = val

    def get_illustrator(self):
        return self._illustrator
Shelf.py

コード:

"""
Shelf
"""

class Shelf:

    def __init__(self):
        self._book_list = []

    def add_book(self, val):
        self._book_list.append(val)

    def get_book_list(self):
        return self._book_list;

main.py

コード:

"""
Main
"""

from Novel import Novel
from Commic import Commic
from Shelf import Shelf

book_shelf = Shelf()

book_shelf.add_book(Novel("nov_title1", "nov_author1", "illustrator1"))
book_shelf.add_book(Commic("com_title1", "com_author1", "magazin1"))
book_shelf.add_book(Novel("nov_title2", "nov_author2", "illustrator2"))
book_shelf.add_book(Commic("com_title2", "com_author2", "magazin2"))

print "Print Title"
print book_shelf.get_book_list()[0].get_title()
print book_shelf.get_book_list()[1].get_title()
print book_shelf.get_book_list()[2].get_title()
print book_shelf.get_book_list()[3].get_title()

print "Call Derivative Method"
print book_shelf.get_book_list()[0].get_illustrator()
print book_shelf.get_book_list()[1].get_magazin()
print book_shelf.get_book_list()[2].get_illustrator()
print book_shelf.get_book_list()[3].get_magazin()

print "Error Statement"
print book_shelf.get_book_list()[0].get_magazin()

出力

コード:

Print Title
nov_title1
com_title1
nov_title2
com_title2
Call Derivative Method
illustrator1
magazin1
illustrator2
magazin2
Error Statement
Traceback (most recent call last):
  File "main.py", line 29, in <module>
    print book_shelf.get_book_list()[0].get_magazin()
AttributeError: Novel instance has no attribute 'get_magazin'

最後の実行文はわざとエラーで落とすための物です.
無いメソドを呼び出せばそこで落ちるだけです。

その他は比較的自由です。本棚を冷蔵庫代わりに使っても
問題ないようなことも出来てしまいます。

ISLe
記事: 2650
登録日時: 15年前
連絡を取る:

Re: C++でのポリモーフィズムについて

#15

投稿記事 by ISLe » 13年前

C++のような静的型付け言語ではフレームワークを作るとき、基底クラスに用意した仮想メンバ関数を呼び出すコードを書きます。

Pythonに限らずスクリプト言語と呼ばれる処理系は実行時に評価されるので、その時点で存在しないメソッドの呼び出しコードを書けます。
そのコードが実行される時点でメソットが実装されていれば呼び出され、実装されていなければ実行時エラー(あるいは例外処理行き)となります。
どこかでメソッドのつづりを間違えていても実行されるところが正しければ動きます。

メソッドの名前参照での実行時結合ですよね。
オーバーライドする仕組みとしても使われますけど。

“あるクラスにしか無い”メソッドという時点でポリモーフィズムの定義から外れる気がするのですけどどうなんでしょうね。
『保守』という言葉に関しては質問にあるリンク先の記事とわたしの感覚とはまったく逆です。

beatle
記事: 1281
登録日時: 14年前
住所: 埼玉
連絡を取る:

Re: C++でのポリモーフィズムについて

#16

投稿記事 by beatle » 13年前

Pythonのコードを拝見しました.なるほど,やりたい事は伝わります.
Pythonのコードで注目すべきなのは,NovelとComicが入ったリスト(_book_list)のどこに何が入っているか,プログラマが把握しているということです.

リストの何番目に何型のインスタンスが入っているか,というのを把握しているからこそ,0番目と2番目に対してNovel特有のget_illustratorを,1番目と3番目に対してget_magazinを,インスタンスが何型なのか明示的に判定せずに呼び出しています.
普通,ポリモーフィズムというのは,「リストに入っている本当の型が何型かは分からないけど,全部Bookを継承していることは保証されているから,Book型とみなして操作できる」というものです.
今回chibagoさんが書かれたPyhonコードでは,リストに追加したインスタンスの本当の型と個数を,プログラマが把握しているため,そもそもポリモーフィズムなんて使わないのです.リストなんか使わずに,Novel型変数とComic型変数をそれぞれ2つずつ用意しても良かったのです.型も数も分かっているのだから,原理的には変数をその分だけ用意すればいいわけです.(ただ記述が少し面倒になるだけ)
ポリモーフィズムが使えるのは,本当の型が分からないけど,全部ある型を継承していることは分かっている,というような場合です.または,わざと基底クラスに対する処理を書くことで,汎用性の高い処理を書きたい,という場合です.

今回のPythonの例は,C++であればタプルを使えばいいでしょう.タプルは,静的に各要素の型と個数が決まっているときに使えます.

chibago

Re: C++でのポリモーフィズムについて

#17

投稿記事 by chibago » 13年前

beatle様、
すみません。私の不注意で誤解を与えてしまったようです。
元々のホームページのものはto_sで出力インターフェースが
統一されていたのですが、Pythonはprint文で並べるだけで
勝手に文字列に変換して表示してくれますので、
直接getメソドで出力させればいいと早とちりしてしまいました。

確かに、これでは、ユーザーがどこにどのインスタンスが入っているか
把握していなければならないことになります。

私が本当にやりたいこととしては、外部にさらすインターフェースは統一
しているが、privateで呼び出す内部処理が派生クラス毎に異なるような
場合です。

私は、オブジェクト指向プログラミングというものが、
オブジェクト同士が対話して処理の流れが自動的に決定していくような
ものとイメージしていますので、そこに、派生型の制約が入ってくるのが
不便と感じてしまいます。外にさらすインターフェースを基底クラスに書き込む
のはぎりぎり許容できるのですが、内部処理の関数名まで基底クラスに合わせて
書き込むのはいかがなものかと感じております。

皆様のご意見をお聞きして、C++にこのような要求をすること自体が
おかしいのかとも思えるようになってきました。

beatle
記事: 1281
登録日時: 14年前
住所: 埼玉
連絡を取る:

Re: C++でのポリモーフィズムについて

#18

投稿記事 by beatle » 13年前

chibago さんが書きました:外にさらすインターフェースを基底クラスに書き込む
のはぎりぎり許容できるのですが、内部処理の関数名まで基底クラスに合わせて
書き込むのはいかがなものかと感じております。
この文がよくわかりません.特に後半が.
外にさらすインターフェースを基底クラスに書きこむのは,is a関係を満たすために重要でして,許容していただけて安心しております.
ここでいう「内部処理」というのは派生クラスに定義するprivate関数のことでしょうか.それとも違うものでしょうか.
派生クラスのprivate関数は基底クラスには影響を与えないはずなので,いくら派生クラスで定義しまくってもらっても構いません.「内部処理の関数名まで基底クラスに合わせて書き込む」の意味が僕には分かりません.

chibago

Re: C++でのポリモーフィズムについて

#19

投稿記事 by chibago » 13年前

beatle様、
お返事ありがとうございます。
「派生クラスのprivate関数は基底クラスには影響を与えないはず」
に関しては私の勉強不足で正確に把握できていなかった部分です。

BOOSTのスマートポインタのようにアップキャストされなければ
派生クラスのprivate関数への内部からのアクセスは問題ない
という理解でよろしいでしょうか。

これなら、私のニーズを比較的満たしていると思われます。

beatle
記事: 1281
登録日時: 14年前
住所: 埼玉
連絡を取る:

Re: C++でのポリモーフィズムについて

#20

投稿記事 by beatle » 13年前

まず,キャストについて誤解していらっしゃるようなので解説しておきます.
基本的に,ポインタをキャストしても,それが指し示している実態は影響を受けません.shared_ptrであっても,生ポインタであっても同様です.
例えばint *p;というポインタpがあったとして,(char *)pとキャストしても,pが指すオブジェクト自体はずっとint型のままです.そのオブジェクトの値も変わりません.これはクラスであっても,スマートポインタであっても同じことです.

次に
chibago さんが書きました:派生クラスのprivate関数への内部からのアクセスは問題ないという理解でよろしいでしょうか。
に対する回答です.
この「内部から」の意味がちょっと良く分かりませんが,派生クラス内部からと仮定すれば,答えは「はい,その理解で正しいです」となります.

コード:

class A
{
public:
    virtual void Interface() = 0;
};

class B : public A
{
public:
    virtual void Interface()
    {
        Func();
    }

private:
    void Func()
    {
        /* ... */
    }
};
こういうことはもちろん可能です.

chibago

Re: C++でのポリモーフィズムについて

#21

投稿記事 by chibago » 13年前

beatle様、
お返事ありがとうございます。

基底クラスの生のポインタに派生クラスのインスタンスを割り当てると自動的にアップキャストされてしまって、
スマートポインタなら回避出来るものと勘違いしておりました。

ほぼ、疑問は解決できました。
ご指導ありがとうございました。

ISLe
記事: 2650
登録日時: 15年前
連絡を取る:

Re: C++でのポリモーフィズムについて

#22

投稿記事 by ISLe » 13年前

件の記事で、比較演算子のオーバーライドが云々書かれてますけど、Novel同士、Comic同士の比較しか考慮されてなくて、NovelとComicの比較はできません。
こんなでは本棚にある本の一覧を表示して並び替えたりすることもできません。
スクリプト言語でも、何らかの比較用の値を求める関数を準備して、その値で比較すると思いますけど。

件の記事は、タイトルにポリモーフィズムと書いてありますけどポリモーフィズムの話はしてないです。

閉鎖

“C言語何でも質問掲示板” へ戻る