質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.31%
C++

C++はC言語をもとにしてつくられた最もよく使われるマルチパラダイムプログラミング言語の1つです。オブジェクト指向、ジェネリック、命令型など広く対応しており、多目的に使用されています。

意見交換

クローズ

6回答

1055閲覧

dynamic_castの多用

AirL_

総合スコア0

C++

C++はC言語をもとにしてつくられた最もよく使われるマルチパラダイムプログラミング言語の1つです。オブジェクト指向、ジェネリック、命令型など広く対応しており、多目的に使用されています。

1グッド

1クリップ

投稿2023/09/03 14:49

1

1

テーマ、知りたいこと

C++においてdynamic_castの多用は避けるべきなのか。

背景、状況

初めて質問させていただきます。
現在C++とDXTKでゲーム開発を行っております。
その中でインターフェースクラスとdynamic_castを多用する形になってしまいました。
そこでdynamic_castの多用について気になったので、ご意見をいただけると幸いです。

GameObjectという基底クラスを継承する形で、機能単位のクラスを実装し、
外部から必要とする振る舞いをインターフェースとして実装して、多重継承させる形で各クラスにもたせています。
以下、簡略化した例です。

C++

1// 基底クラス 2class GameObject 3{ 4 // 座標、回転、スケールなどの共通して使う情報 5} 6 7// シャドウマップへの描画を行うインターフェース 8struct IShadable 9{ 10 virtual ~IShadable(); 11 virtual void RenderShadowMap() = 0; // シャドウマップに描画を行う関数 12} 13 14// モデルを描画するオブジェクト 15class ModelObject : public GameObject, public IShadable 16{ 17 ... 18 // シャドウマップに描画を行う関数 19 void RenderShadowMap() override; 20}

そしてこのオブジェクトたちをGameObjectのポインタ配列として保持させています。
描画を行う際などは、配列内の要素をdynamic_castを使用してインターフェースを実装しているなら処理を行うようにしていました。
以下、例です。

C++

1// シーンクラス内 2std::vector<std::unique_ptr<GameObject>> m_gameObjects; 3 4// モデルオブジェクトを追加 5m_gameObjects.push_back(std::make_unique<ModelObject>(...)); 6 7 8// シャドウマップに描画を行う 9for(const GameObject& obj : m_gameObjects) 10{ 11 IShadable* shadable = dynamic_cast<IShadable*>(obj.get()); 12 if(shadable) 13 { 14 shadable->RenderShadowMap(); 15 } 16}

この設計だと、どうしてもインターフェースにアクセスするためにはdynamic_castを使用する事になってしまいます。

そこで、そもそもの話としてdynamic_castの多用は避けるべきだと思いますか?
避けるべきとした時、より良くなる実装はどのような形が良くなると思われますか?

拙い文章ですが、ご教授いただけると幸いです。
よろしくお願いします。

fanaを押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

回答6

#1

fana

総合スコア12151

投稿2023/09/04 01:35

std::vector<std::unique_ptr<GameObject>> m_gameObjects;

というデータの保持方法が must だということなのであれば,「dynamic_castを使うしかない」みたいな状況なのかもしれませんが…
IShadable についてのみ行いたい処理が存在するのであれば,「IShadableだけの集合」を保持すれば(全オブジェクトを走査して型を調べて…ということをせずとも)済むように思えます.

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#2

AirL_

総合スコア0

投稿2023/09/04 12:07

#1
ご意見、ありがとうございます。

確かに、予めインターフェースのポインタで配列を作るのは良いと思います。
しかし、これですとインターフェースが増えていくと、それに比例して各インターフェースのリストを用意することになってしまうのではないかと思います。
メンバ変数をあまり増やしたくはないので、利用頻度の高いものだけにするなど工夫が必要そうですね………

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#3

fana

総合スコア12151

投稿2023/09/05 01:24

#2

インターフェースが増えていくと、それに比例して各インターフェースのリストを用意することになってしまう

とはいえ,現状(=提示されたコード)では,最低でもインタフェースの種類の分だけの dynamic_cast を用いた全要素ループなコードを用意せねばならないわけで…

うーん…… 例えば,現在では「シーンクラス」内にあるのだと思われる

// シャドウマップに描画を行う

という処理を,↓みたいな感じに別クラスに切り出すようなことを考えてみると,その中に「インターフェースのリストを用意する」ことになる点については違和感がないのでは? …とか思いますが,どうなんでしょう.

C++

1//Register()メソッドで登録されたオブジェクト群を用いて「シャドウマップに描画を行う」処理を実施するやつ. 2//(「シーンクラス」がこれをメンバに保持して使うような形を想像すれば良いかと) 3class ShadowMapRenderer 4{ 5public: 6 void Register( std::weak_ptr<IShadable> wpShadable ) 7 { /*引数をメンバ m_List に追加*/ } 8 9 void Render() 10 { 11 //寿命が尽きている要素は m_List から削除, 12 //そうでない要素の RenderShadowMap() を呼ぶ. 13 } 14private: 15 std::vector< std::weak_ptr<IShadable> > m_List; //←これが自然と必要になる 16};

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#4

fana

総合スコア12151

投稿2023/09/05 02:00

編集2023/09/05 02:08

「Visitorパターン」を使う手もあるかな,とか思ったけども,dynamic_cast と比べてコレはどうなの? っていう観点については私はわからないです(調べるとか有識者に問うとかしていただきたく).

一応コードを書いてはみたけど,こんな感じなのかな…?

C++

1//--------------------------- 2//GameObject型の定義 3class Visitor; //class宣言 4 5class GameObject 6{ 7public: 8 virtual ~GameObject() = default; 9 //※こんな仮想関数を用意 10 virtual void Accept( Visitor &visitor ) = 0; 11}; 12 13//--------------------------- 14//interface 群(質問文の IShadable みたいな) 15struct IA 16{ 17 virtual ~IA() = default; 18 virtual void Do_A() = 0; 19}; 20 21struct IB 22{ 23 virtual ~IB() = default; 24 virtual void Do_B() = 0; 25}; 26 27//--------------------------- 28//Visitor型の定義 29class Visitor 30{ 31public: 32 virtual ~Visitor() = default; 33 //※interface の種類が増えたらここのメソッドも増やさないとならない 34 virtual void Visit( IA & ){} 35 virtual void Visit( IB & ){} 36}; 37 38//--------------------------- 39//テスト用の,GameObject と interface を継承した型 40class ConcreteA : public GameObject, public IA 41{ 42public: 43 ConcreteA( const std::string &Name ) : m_Name(Name) {} 44 virtual void Accept( Visitor &visitor ) override { visitor.Visit( *this ); } 45 virtual void Do_A() override { std::cout << m_Name << ".Do_A()\n"; } 46private: 47 std::string m_Name; 48}; 49 50class ConcreteB : public GameObject, public IB 51{ 52public: 53 ConcreteB( const std::string &Name ) : m_Name(Name) {} 54 virtual void Accept( Visitor &visitor ) override { visitor.Visit( *this ); } 55 virtual void Do_B() override { std::cout << m_Name << ".Do_B()\n"; } 56private: 57 std::string m_Name; 58}; 59 60//--------------------------- 61//Visitor を継承した型 62 63//IA::Do_A() を呼ぶだけの仕事をするやつ 64class DoA_Visitor : public Visitor 65{ 66public: 67 virtual void Visit( IA &a ) override { a.Do_A(); } 68}; 69 70//IB::Do_B() を呼ぶだけの仕事をするやつ 71class DoB_Visitor : public Visitor 72{ 73public: 74 virtual void Visit( IB &b ) override { b.Do_B(); } 75}; 76 77//--------------------------- 78//テスト 79int main( void ) 80{ 81 std::vector< std::unique_ptr<GameObject> > Objs; 82 Objs.reserve( 5 ); 83 Objs.push_back( std::make_unique<ConcreteA>( "A1" ) ); 84 Objs.push_back( std::make_unique<ConcreteA>( "A2" ) ); 85 Objs.push_back( std::make_unique<ConcreteB>( "B1" ) ); 86 Objs.push_back( std::make_unique<ConcreteA>( "A3" ) ); 87 Objs.push_back( std::make_unique<ConcreteB>( "B2" ) ); 88 89 DoA_Visitor VA; 90 for( auto &up : Objs ){ up->Accept(VA); } 91 92 std::cout << "---" << std::endl; 93 94 DoB_Visitor VB; 95 for( auto &up : Objs ){ up->Accept(VB); } 96 97 return 0; 98}

[追記]
あー,でもこれだと,「IA と IB の両方を継承しているやつ」とかが扱えないからダメっぽいな…

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#5

int32_t

総合スコア21927

投稿2023/09/05 02:03

編集2023/09/05 02:05

dynamic_cast を使っていて期待通りに動いているならそのままで構わないと思います。速度やコードサイズの問題が出てきたらそのときに対処をすればよいのです。


dynamic_cast は遅いとかコードサイズが増えるとか参照で使いにくいとかで避けられがちな印象です。私が仕事で関わったC++のプロジェクトではすべて RTTI が禁止されていたので dynamic_cast は使えませんでした。

dynamic_cast に限らずキャストはできるだけ避け、virtual 関数で何とかする方が好まれる気がします。

一例:

c++

1class GameObject { 2 virtual IShadable* AsShadable() { 3 return nullptr; 4 } 5} 6 7// private継承も微妙なので、ModelObject用の IShadable の実装は 8// 別クラスにしたほうがいいかも 9class ModelObject : public GameObject, private IShadable { 10 ... 11 IShadable* AsShadable() override { 12 return this; 13 } 14 // シャドウマップに描画を行う関数 15 void RenderShadowMap() override; 16} 17 18... 19 20// シャドウマップに描画を行う 21for (const auto& obj : m_gameObjects) { 22 if (auto* shadable = obj->AsShadable()) { 23 shadable->RenderShadowMap(); 24 } 25}

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#6

SaitoAtsushi

総合スコア5714

投稿2023/09/06 07:41

C++ の設計者である Bjarne Stroustrup 氏は彼の著書である C++ の設計と進化の中で型を if 文で検査して処理を分けるような方法に対して強い否定の態度を示しています。 型ごとに振る舞いが違うのであればそれは型の中に隠蔽されているべきであって、使う側で区別する必要があるのなら悪い抽象化です。

その一方では、 C++ の仕様はただ言語の仕様を決めるのみであって何が正しいのか決めることはしないし、正しいことしか出来ないように制約することもしないとも Stroustrup 氏は述べています。 色々と事情があるのでいつも「正しく」することは出来ません。 汚くても汚いなりになんとかできるのも C++ の良いところです。

仮に dynamic_cast の多用が一般的な作法として好ましくないのだとしても C++ はそれを許す思想だと言えるでしょう。

ひとつのプロジェクトの中ではある程度には一貫した設計にする必要はあると思いますが、それが常道であるかどうかはそんなに気にしなくてよいように思います。


余談です。

共有オブジェクト (ダイナミックリンクライブラリ) はその構造上、名前とアドレスの組だけで外部とのインターフェイス情報が保存されています。 型に関する情報は別の方法 (C や C++ ではヘッダ) でやりとりするということで整合性を保っていましたが、オブジェクト指向を前提としたやりとりで再利用性の高いコンポーネントのために Component Object Model という考え方が発明されました。

COM におけるインターフェイスは C++ の抽象クラスに対応しており、ひとつのオブジェクトが様々な側面 (インターフェイス) を持つのが当たり前の世界観です。

ひとつのオブジェクトを異なるインターフェイスを通じて使うという点で COM の考え方に似ているように思えるので参考になるかもしれません。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

最新の回答から1ヶ月経過したため この意見交換はクローズされました

意見をやりとりしたい話題がある場合は質問してみましょう!

質問する

関連した質問