前の質問は こちら です。文字数上限に達してしまったため、続きをここに記述します。
追記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 の文章が言いたいことというのは、
- 一般的に C++ で想定されている部分型ポリモルフィズムの実現方法がメンバ関数としてのシングルディスパッチであり、それではマルチディスパッチは実現できないということ。
- 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}
仮想関数テーブルによりシングルディスパッチが可能であることは理解しています。質問はそこではなくて、
- 今の例のような
std::variant
, もしくはもっと洗練された何かを使って多重ディスパッチを実現することが可能ではないか? - この実装では 明示的に
DogOrCat
を作成しているが、Animal
をDog
とCat
が継承しているときに、自動的にそれに対応するstd::variant
を生成できるのか? - この実装では長ったらしい表現(
std::variant
やstd::visit
)を書かなければいけないが、避けることはできないのか? - この実装のポリモルフィズムは実践的に問題となる点(パフォーマンスやバグ、表現力が不十分であるなど)を抱えているのか?
- 上記の点が全てクリアされているならば、Julia と同じぐらいの表現力で多重ディスパッチ可能であると言えるのではないか?
- そうであるならばもとの Julia の文章はこの方式によるポリモルフィズムを想定していない or 不適切であるということになるのではないか?
ということです。