C++は静的に型付けを行うので、基本的に実行時に型を切り替えることはできない。なので、入力に応じて異なる型を返すことはできない。だが継承を使っているなら、常に基底クラスへのポインタを返しつつ、入力に応じて派生クラスのテンプレート引数を変更することは可能だ。
例えば、以下のようなものがあるとしよう。
struct Base { virtual ~Base() = default; }; template<typename T> struct Derived : public Base { ~Derived() override = default; };
ここで、例えば入力によってT
の型を変えたいとする。これは簡単で、以下のようにすればよい。
std::unique_ptr<Base> read_input(const std::map<std::string, std::string>& input) { if(input.at("type") == "double") { return std::make_unique<Derived<double>>(); } else if(input.at("type") == "float") { return std::make_unique<Derived<float>>(); } else { throw std::runtime_error("unexpected type appears: "s + input.at("type")); } }
必要なら、このような関数を入れ子にしていけばよい。
template<typename T1, typename T2> struct Derived : public Base { ~Derived() override = default; }; // read_T1でT1を決め、この関数に型情報を渡して呼ぶ template<typename T1> std::unique_ptr<Base> read_T2(const std::map<std::string, std::string>& input) { if(input.at("type2") == "double") { return std::make_unique<Derived<T1, double>>(); } else if(input.at("type2") == "float") { return std::make_unique<Derived<T1, float>>(); } else { throw std::runtime_error("unexpected type appears: "s + input.at("type2")); } } std::unique_ptr<Base> read_T1(const std::map<std::string, std::string>& input) { if(input.at("type1") == "i32") { // read_T2にT1の分の型情報を渡す return read_T2<std::int32_t>(input); } else if(input.at("type1") == "u32") { return read_T2<std::uint32_t>(input); } else { throw std::runtime_error("unexpected type appears: "s + input.at("type1")); } }
全く同じ型のセットが複数あるなら、以下のようにできる。そういう状況はあまりなさそうではあるが、まだあり得そうな場合として、何かの機能をオンオフするbool
テンプレート引数を複数取るということにしよう。この状況ならとり得るオプションがon
かoff
しかないので、同じ分岐が何度も続くことになる。そういうときに何度も同じコードを書くのはタルいので、再帰すればよい。このとき、当然ながら参照するべきキーが異なるはずなので、それは再帰する度に消費していかないといけない。
template<bool Cond1, bool Cond2, bool Cond3, bool Cond4> struct Derived : public Base { ~Derived() override = default; }; // 条件が4つ決まっていたらコンストラクタを呼ぶ template<bool ... Cs> typename std::enable_if< sizeof...(Cs) == 4, std::shared_ptr<Base>>::type read_input(const std::map<std::string, std::string>&, std::vector<std::string>) { return std::make_shared<Derived<Cs...>>(); } // まだ4つ決まっていなければ、次を決める template<bool ... Cs> typename std::enable_if< sizeof...(Cs) < 4, std::shared_ptr<Base>>::type read_input(const std::map<std::string, std::string>& input, std::vector<std::string> keys) { // 4つあるはずのキーのリストがなくなっていたらエラー if(keys.empty()) { throw std::out_of_range("missing option"); } // 最後尾のキーを取得し、pop_backしておく const auto key = keys.back(); keys.pop_back(); if(input.at(key) == "on") { // 現時点での最後尾のものが決まる // (既に決まっているCs...は以前pop_backしたものなので後ろに来る) return read_input<true, Cs...>(input, std::move(keys)); } else if(input.at(key) == "off") { return read_input<false, Cs...>(input, std::move(keys)); } else { throw std::runtime_error("unknown option : " + input.at(key)); } } int main() { const std::map<std::string, std::string> input{ {"opt1", "on"}, {"opt2", "off"}, {"opt3", "on"}, {"opt4", "off"} }; const auto derived = read_input(input, {"opt1", "opt2", "opt3", "opt4"}); }
上のコードはC++11でも動くはずだが、C++17を使えば少し楽になる。if constexpr
が使えるからだ。
template<bool ... Cs> std::shared_ptr<Base> read_input(const std::map<std::string, std::string>& input, std::vector<std::string> keys) { if constexpr (sizeof...(Cs) == 4) { return std::make_shared<Derived<Cs...>>(); } else { if(keys.empty()) { throw std::out_of_range("missing option"); } const auto key = keys.back(); keys.pop_back(); if(input.at(key) == "on") { return read_input<true, Cs...>(input, std::move(keys)); } else if(input.at(key) == "off") { return read_input<false, Cs...>(input, std::move(keys)); } else { throw std::runtime_error("unknown option : " + input.at(key)); } } }
if constexpr
を使えば、再帰を終える場合のSFINAEとオーバーロードが必要なくなる。ここで普通のif
を使うと、あり得る全ての個数のCs
に対してDerived<Cs...>
を実体化しようとしてしまう。Derived
は4つのパラメータを取るので、当然4以外の個数のパラメータが来るとエラーになる。なのでここではsizeof...(Cs) == 4
以外のときの実体化を抑制しなければならず、普通のif
ではなくif constexpr
(か、SFINAE
)を使う必要がある。
普段パラメータパック展開をする際は受け取る側もパラメータパックを受け取ることが多い。なので上のようなコードが規格的に許されているのか確信が持てず、N3337をちょっと見てみた。だが検索してパラパラ見た限りではその場合に対して陽に言及したところは見つからなかった。パック展開に関してのところをざっと読んだ限りでは、単にそこに(文脈に応じた形で)展開されて終わりのように読める。単に展開されることしか定められていないなら、受け取る側が何であるかは関係ないだろう。もし展開結果がエラーになるのなら、エラーになって終わりだ。それ以上のことを細かく決める必要もなさそうだ。だとすると、上のコードは特に何も問題ないだろう。
可能な限りテンプレートで静的に決めておきたいが実行時に分岐もしないといけないような場合は、こういう選択肢もある。メリットは静的に決めることでコンパイラに最適化の機会を与えられること、デメリットは全てのパターンが生成されるのでコンパイル時間が爆発することだ。実行時に分けてもパフォーマンス的に影響がない、またはよりパフォーマンスが向上するところを上手く選んでテンプレートから継承に切り替えるとよいだろう。