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

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

新規登録して質問してみよう
ただいま回答率
85.35%
継承

継承(インヘリタンス)はオブジェクト指向プログラミングに存在するシステムです。継承はオブジェクトが各自定義する必要をなくし、継承元のオブジェクトで定義されている内容を引き継ぎます。

C++

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

Julia

Juliaとは、科学技術計算に特化した、高水準・高性能な動的プログラミング言語です。オープンソースとして公表されており、書き易く動きが早いことが特徴です。

Q&A

2回答

3599閲覧

C++ で多重ディスパッチをする方法と Julia 2

Paalon

総合スコア266

継承

継承(インヘリタンス)はオブジェクト指向プログラミングに存在するシステムです。継承はオブジェクトが各自定義する必要をなくし、継承元のオブジェクトで定義されている内容を引き継ぎます。

C++

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

Julia

Juliaとは、科学技術計算に特化した、高水準・高性能な動的プログラミング言語です。オープンソースとして公表されており、書き易く動きが早いことが特徴です。

0グッド

0クリップ

投稿2020/03/20 18:43

編集2020/03/21 05:19

前の質問は こちら です。文字数上限に達してしまったため、続きをここに記述します。

追記3

色々理解が整理できたので質問し直します。C++ ではメンバ関数として仮想関数を実装すれば、例えば、次のように

cpp

1#include <iostream> 2#include <vector> 3#include <memory> 4 5struct Animal { 6 virtual ~Animal() = 0; 7 virtual void printlntype() { 8 std::cout << "Animal" << std::endl; 9 } 10}; 11Animal::~Animal() {} 12 13struct Dog final : Animal { 14 void printlntype() override { 15 std::cout << "Dog" << std::endl; 16 } 17}; 18 19struct Cat final : Animal { 20 // void printlntype() override { 21 // std::cout << "Cat" << std::endl; 22 // } 23}; 24 25int main() { 26 27 auto dog = Dog(); 28 auto cat = Cat(); 29 dog.printlntype(); 30 cat.printlntype(); 31 32 auto animals = std::vector<std::shared_ptr<Animal>>{ 33 std::make_shared<Dog>(), 34 std::make_shared<Cat>() 35 }; 36 37 for (auto const & animal: animals) { 38 animal->printlntype(); 39 } 40 41}

スーパー型 Animal のポインタとして呼び出された関数はもともと?部分型 Dog だったときオーバーロードされるので、1変数(クラスについての)部分型ポリモルフィズムができ、配列などそのポリモルフィズムを実現できることは分かります。

多変数で部分型ポリモルフィズムを考えるときには、(1変数での実装しかできない)メンバ関数のポリモルフィズムを使うわけにはいかないので、普通の関数として実装することになります。以下のように実装すれば(Animal 型を継承する全ての型を入れた std::variant 的なものが自動で作れるのかどうかを除いて)、

cpp

1/* 2g++-HEAD -std=c++2a multiple_dispatch.cpp 3./a.out 4*/ 5 6#include <concepts> 7#include <iostream> 8#include <vector> 9#include <variant> 10#include <memory> 11 12struct Animal { 13 virtual ~Animal() = 0; 14}; 15Animal::~Animal() {} 16 17struct Dog final : Animal {}; 18 19struct Cat final : Animal {}; 20 21using SubAnimal = std::variant<Dog, Cat>; 22 23// one variable 24 25template <std::derived_from<Animal> T> 26auto printlntype(T const &) { 27 std::cout << "Animal" << std::endl; 28} 29 30auto printlntype(Dog const &) { 31 std::cout << "Dog" << std::endl; 32} 33 34// auto printtype(Cat const &) { 35// std::cout << "Cat" << std::endl; 36// } 37 38// two variables 39 40template <std::derived_from<Animal> T> 41auto printlntype(T const &, T const &) { 42 std::cout << "Animal Animal" << std::endl; 43} 44 45template <std::derived_from<Animal> T> 46auto printlntype(Dog const &, T const &) { 47 std::cout << "Dog Animal" << std::endl; 48} 49 50template <std::derived_from<Animal> T> 51auto printlntype(T const &, Dog const &) { 52 std::cout << "Animal Dog" << std::endl; 53} 54 55auto printlntype(Dog const &, Dog const &) { 56 std::cout << "Dog Dog" << std::endl; 57} 58 59int main() { 60 auto dog = Dog(); 61 auto cat = Cat(); 62 63 // single dispatch 64 printlntype(dog); 65 printlntype(cat); 66 67 // double dispatch 68 printlntype(dog, dog); 69 printlntype(dog, cat); 70 printlntype(cat, dog); 71 printlntype(cat, cat); 72 73 auto animals = std::vector<std::shared_ptr<std::variant<Dog, Cat>>>{ 74 std::make_shared<SubAnimal>(SubAnimal(Dog())), 75 std::make_shared<SubAnimal>(SubAnimal(Cat())) 76 }; 77 78 // single dispatch 79 for (auto const & animal: animals) { 80 std::visit( 81 [] (const auto & x) { 82 printlntype(x); 83 }, 84 *animal 85 ); 86 } 87 88 // double dispatch 89 for (auto const & v_animal1: animals) { 90 for (auto const & v_animal2: animals) { 91 std::visit( 92 [&] (const auto & animal1) { 93 std::visit( 94 [&] (const auto & animal2) { 95 printlntype(animal1, animal2); 96 }, 97 *v_animal2 98 ); 99 }, 100 *v_animal1 101 ); 102 } 103 } 104} 105

実行結果は

txt

1Dog 2Animal 3Dog Dog 4Dog Animal 5Animal Dog 6Animal Animal 7Dog 8Animal 9Dog Dog 10Dog Animal 11Animal Dog 12Animal Animal

となり、多変数部分型ポリモルフィズム、つまりマルチディスパッチが実現できると思います。Julia の文章が言いたいことというのは、

  1. 一般的に C++ で想定されている部分型ポリモルフィズムの実現方法がメンバ関数としてのシングルディスパッチであり、それではマルチディスパッチは実現できないということ。
  2. C++ でマルチディスパッチを実現しようとすればできるが、パラメトリックポリモルフィズムが絡んだとき、つまり、配列などのコンテナの要素の型に関して部分型ポリモルフィズムをするときには、std::variant を使ったようなコードを書く必要があり、そのあたりのコードがパフォーマンス的によろしくない、もしくは自動的に subtyping された std::variant 的なものが作れないためにコードが煩雑であり、現実的でない?どちらにせよ、あまり一般的に(まだ?)使われていない。

ということなんでしょうか?


** 追記4 **:

Julia だと確かに以下のように if ... else .. を使ったものでもそうでなくてもコンパイルされた機械語は全く同じです。

julia

1mutable struct Dog end 2mutable struct Cat end 3foo(::Dog) = println("Dog") 4foo(::Cat) = println("Cat") 5 6t = 0 7a = if t == 0 Dog() else Cat() end 8b = Dog() 9 10@code_native foo(a) 11@code_native foo(b)

ただ C++ でも等価な機械語になるかどうかは分かりませんが、if ... else ... を使ったコードは以下のように書けますよね?

cpp

1#include <iostream> 2#include <variant> 3 4struct Dog {}; 5struct Cat {}; 6using DogOrCat = std::variant<Dog, Cat>; 7 8auto foo(Dog const &) { 9 std::cout << "Dog" << std::endl; 10} 11auto foo(Cat const &) { 12 std::cout << "Cat" << std::endl; 13} 14 15int main() { 16 auto t = 0; 17 auto a = [&] () { 18 if (t == 0) { 19 return DogOrCat(Dog()); 20 } 21 return DogOrCat(Cat()); 22 } (); 23 std::visit( 24 [] (auto const & x) { 25 foo(x); 26 }, 27 a 28 ); 29}

仮想関数テーブルによりシングルディスパッチが可能であることは理解しています。質問はそこではなくて、

  1. 今の例のような std::variant, もしくはもっと洗練された何かを使って多重ディスパッチを実現することが可能ではないか?
  2. この実装では 明示的に DogOrCat を作成しているが、AnimalDogCat が継承しているときに、自動的にそれに対応する std::variant を生成できるのか?
  3. この実装では長ったらしい表現(std::variantstd::visit)を書かなければいけないが、避けることはできないのか?
  4. この実装のポリモルフィズムは実践的に問題となる点(パフォーマンスやバグ、表現力が不十分であるなど)を抱えているのか?
  5. 上記の点が全てクリアされているならば、Julia と同じぐらいの表現力で多重ディスパッチ可能であると言えるのではないか?
  6. そうであるならばもとの Julia の文章はこの方式によるポリモルフィズムを想定していない or 不適切であるということになるのではないか?

ということです。

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

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

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

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

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

izmktr

2020/03/20 19:24

もしかしたら私の勘違いかもしれませんが、 auto dog = Dog() と auto dog = new Dog() と Animal *dog = new Dog() の違いが問題になる話じゃないでしょうか? 本来書かないといけないサンプルは後ろの2個のどちらかのように思います
guest

回答2

0

こんにちは。

なるほど。実引数の型ではなく、実引数のインスタンスの型に応じてオーバーロードしたいのですね。

静的型付けの言語が本質的には対応しない機能です。オーバーロード解決はコンパイル時に行われるため、コンパイル時に型が決まっている必要があります。しかし、実引数のインスタンスの型は実行時に決まります。

しかし、事前に対応する型のリストを決定することで、(恐らくconceptを待たなくても)ご提示のような方法で記述することは可能ですね。
std::visitは多変数に対応していますので、これは正に多重ディスパッチと思います。

C++

1 for (auto const & v_animal1: animals) { 2 for (auto const & v_animal2: animals) { 3 std::visit( 4 [&] (const auto & animal1, const auto & animal2) { 5 printlntype(animal1, animal2); 6 }, 7 *v_animal1, 8 *v_animal2 9 ); 10 } 11 }

st::visit呼び出しや余計なラムダ式定義も必要なので使い勝手はいまいちですが、マクロを使って誤魔化すことも可能です。

C++

1#define PRINT_INT_TYPE(A, B)\ 2 std::visit(\ 3 [] (const auto & animal1, const auto & animal2) {\ 4 printlntype(animal1, animal2);\ 5 },\ 6 A,\ 7 B\ 8 ) 9 10 (中略) 11 12 for (auto const & v_animal1: animals) { 13 for (auto const & v_animal2: animals) { 14 PRINT_INT_TYPE(*v_animal1, *v_animal2); 15 }

なお、C++は静的型付け言語ですので、using SubAnimal = std::variant<Dog, Cat>; のような方法で対応する型を指定することが必須です。これによりコンパイル時に必要な型の組み合わせを自動的に網羅することができ、リストから外れた型を指定した時にコンパイル・エラーにすることができます。
この機能があるので、ご提示のソースのAnimalクラスやstd::derived_fromも必要ないと思います。
wandbox

そして、動的型付け言語の場合は、実行時にオーバーロード解決されるため容易に実現可能ですね。
当たり前ですが、実行時に条件分岐するので、その分遅いです。もちろん、C++の上記実装も実行時に仮想関数呼び出しを行いますので、その分のオーバーヘッドがあります。

さて、この問題は、静的型付けと動的型付けのどちらを使うかの問題と思います。
一般に動的型付け言語は静的型付け言語に比べて、コンパイル時のバグのチェック能力が低いです。(当たり前ですが、実行に決まる型をコンパイル時にチェックすることはできませんから)
ですので、大規模開発には向きません。逆に小規模開発の時は面倒な手順が少ないので便利です。

追記への回答

ちんたら書いているうちに追記がありましたので、回答してみます。

  1. 今の例のような std::variant, もしくはもっと洗練された何かを使って多重ディスパッチを実現することが可能ではないか?

お使いの機能はconceptという今正に標準規格へ取り入れることが議論されている機能です。
つまり、現時点ではもっとも洗練されている方法と思います。

  1. この実装では 明示的に DogOrCat を作成しているが、Animal を Dog と Cat が継承しているときに、自動的にそれに対応する std::variant を生成できるのか?

現時点では無理と思います。インクルードしているヘッダの相違でst::variantが取り込む型が変わってしまうことになるので、将来的にも対応しないような気がします。

  1. この実装では長ったらしい表現(std::variant や std::visit)を書かなければいけないが、避けることはできないのか?

C++は、「長ったらしい表現」をある意味推奨しているので避けることは困難と思います。
マクロを使うことで隠すことはできるでしょうが、却って可読性を落とす危険もあるので使う時は要注意です。

  1. この実装のポリモルフィズムは実践的に問題となる点(パフォーマンスやバグ、表現力が不十分であるなど)を抱えているのか?

私に分かる範囲では無いように感じます。今、標準規格を制定する人たちが深い議論を進めている筈です。彼らが採用と決定したら、安心して使っても、まず大丈夫だろうと思います。

  1. 上記の点が全てクリアされているならば、Julia と同じぐらいの表現力で多重ディスパッチ可能であると言えるのではないか?

全てを完全にクリアできるとは思えないですが、多重ディスパッチ可能と言える程度にはクリアできているように感じます。

  1. そうであるならばもとの Julia の文章はこの方式によるポリモルフィズムを想定していない or 不適切であるということになるのではないか?

その通りと思います。
動的型付け言語は自由度が高いですから、そのような実装は得意です。その代わり、バグ検出能力を犠牲にします。ですので、簡単にプログラムできることを取るのか、大規模プログラムを開発できることを取るのかの選択となります。どちらが優れているという話ではなく、どのようなプログラム開発に向いているのかという話です。

投稿2020/03/21 06:01

編集2020/03/21 06:21
Chironian

総合スコア23272

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

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

0

Julia の文法知らないのでC++風に書きますが
多分、Julia ではこれができるんだと思います

auto a = type == 0 ? Animal() : Dog(); // C++ではNG foo(a); //type == 0 ならfoo(Animal) が呼ばれる foo(a); //type == 1 ならfoo(Dog) が呼ばれる

C++は型があるので、最初の条件演算子で
autoを使おうとも、trueとfalseの型が違うのでエラーになります

これはnewを使ってポインタにすればうまくいきます

Animal *a = type == 0 ? new Animal() : new Dog(); // OK foo(a); // typeに関わらず、必ずfoo(Animal *)が呼ばれる

なぜこうなるかというと、aの型がAnimal*だからです
ここをautoにすることは出来ません

ちなみに、大問題が発生しますが、以下のような書き方ならFoo(Dog*)が呼ばれます

auto a = type == 0 ? (Dog *) new Animal() : new Dog(); // OK foo(a); // typeに関わらず、必ずfoo(Dog *)が呼ばれる

aの変数の中を見て、ではなくautoによって型が確定しているので、
その型を見て「コンパイル時に」どの関数を呼び出すか確定しています
ポリモルフィズムをするなら「実行時に」どの関数を呼び出す確定しないといけません

実行時にどの関数を呼び出すか決める場合、
この関数は分岐する可能性があるという表を作る必要があります
それを指定するのがvirtualというキーワードです

仮想関数テーブルで調べてもらえれば、C++ではどうやって実現していて
普通の関数に適用できない理由もわかると思います

投稿2020/03/21 00:44

izmktr

総合スコア2856

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

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

Paalon

2020/03/21 05:20

追記4にコメントを書きました。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

まだベストアンサーが選ばれていません

会員登録して回答してみよう

アカウントをお持ちの方は

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

ただいまの回答率
85.35%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問