を、たまにコードを見たりしている後輩が自分のためのコードの中でよくやっている。要するにこういうことだ。
std::vector<double> v1; std::vector<double> v2; // do something... assert(v1.size() == v2.size()); std::vector<double> v3(v1.size()); for(std::size_t i=0; i<v1.size(); ++i) v3.at(i) = v1.at(i) + v2.at(i);
善良なC++erとしてまず教えたのはstd::valarray
の存在である。valarray
は中々に便利だと思うのだが、何故か知名度がない。実際私も使っていない。これで全て解決だと思ったが、このvector
が故あって2重、3重になることがあるらしい。
std::vector<std::vector<double>> v1; std::vector<std::vector<double>> v2; // do something... assert(v1.size() == v2.size()); std::vector<std::vector<double>> v3(v1.size()); for(std::size_t i=0; i<v1.size(); ++i) { assert(v1.at(i).size() == v2.at(i).size()); v3.at(i) = std::vector<double>(v1.at(i).size()); for(std::size_t j=0; j<v1.at(i).size(); ++j) { v3.at(i) = v1.at(i) + v2.at(i); } }
データファイルなどを読み取った時点でvector
のサイズがどの次元においても決定するようなものだった場合、個人的には全要素数の総和個の要素を持つvector
なりvalarray
を用い、必要ならそれを薄くラップしてvalueT operator()(std::size_t i, std::size_t j)
などを定義するのが最善であろうと思う。そしてoperator+
ではその本体のコンテナを普通に足せば良い。
だがこのクラスは掛け算をすると行列のように振る舞うこともあるらしく、そこまで来るならEigen
かBoost.uBlas
を使うのが良いとは言ったが、そこそこ書き進めているため既にstd::vector<T>
用のoperator
を定義しており、今から2重の場合に拡張するとのことだった。しかも、このvector
のvalue_type
はdouble
にもint
にもなり得るし、その上それらを演算することもあると言うではないか!
こういう演算子を自分で用意するのは生産性や速度、バグ取り的な観点からは論外であるが、練習・教育目的なら許される。そういう状況からすれば、どうせ拡張するなら一撃で終わらせられるようにしたい。template
を使えるなら、次元数が合っている場合N次元のvector
の演算は容易だ。毎回引数よりも一つ次元の低いvector
に対するインスタンスが形成され、順次適用されて話が済むだろう。
template<typename T> std::vector<T> operator+(std::vector<T> const& lhs, std::vector<T> const& rhs) { if(lhs.size() != rhs.size()) throw std::out_of_range("operator+: different size"); std::vector<T> retval(lhs.size()); auto iter1 = lhs.cbegin(); auto iter2 = rhs.cbegin(); auto iter3 = retval.begin(); while(iter1 != lhs.cend() && iter2 != rhs.cend() && iter3 != retval.end()) { *iter3 = (*iter1) + (*iter2); ++iter1; ++iter2; ++iter3; } return retval; }
さて、これだと問題がある。vector
の型引数が等しくないと機能しないのだ。int
とdouble
のvector
を足し算することはすでに要件に入っている。となると引数を増やすべきだろう。今回はallocator
のことまでは考えないことにして、T1
とT2
を用意する。
template<typename T1, typename T2> std::vector<T1> operator+(std::vector<T1> const& lhs, std::vector<T2> const& rhs) { // impl. }
ところで戻り値の型はこれでいいのだろうか。例えばint
とdouble
を足してdouble
になって欲しい時、引数の順序に気をつけるというのは正気の沙汰ではない。ではtemplate
型引数を増やして、ユーザーが戻り値型を明示的に定義し、各要素の演算はstatic_cast
するべきだろうか。それが一番柔軟性が高いが、少々使い勝手が犠牲になるし、ユーザーがcast不能な型を持ってきた場合の対応が微妙だ。int
とdouble
を足してint
になって欲しい、というケースは(今回のコードでは)ないと思っていいということもあって、要素同士のoperator+
の結果が返るのが妥当な選択だろうということになった。
となると少し面倒だ。例えば、何らかの理由(ジェネリックプログラミングをやっているなど)でadd
関数を作るなどというときは、以下のように型を定義すると思う、というか以前実際した。
template<typename T1, typename T2> constexpr inline auto add(T1&& lhs, T2&& rhs) -> decltype(std::forward<T1>(lhs) + std::forward<T2>(rhs)) { return std::forward<T1>(lhs) + std::forward<T2>(rhs); }
最初、何故かoperator+
はstd::vector<T1>
は受け取るがT1
は受け取らないので、これと同じ方法では演算結果の型を推論できないと思ってしまった。そして考えたのは、上記のようなadd
関数の戻り値型を使えばいいということだった。add
関数はこちらが明示的にtemplate
型引数を与えてやればその戻り値型も確定する。つまり、
template<typename T1, typename T2> inline std::vector<typename result_type_of<decltype(add<T1, T2>)>::type> operator+(const std::vector<T1>& lhs, const std::vector<T2>& rhs) { // impl. }
のようなことができれば良い。そのためにはresult_type_of
が必要で、それは
template<typename F> struct result_type_of { typedef typename std::function<F>::result_type type; };
でできる。と、ここまで勢いで書いて思ったのだが、確か標準にstd::result_of
か何かそんなものがあったはずだ。再発明よりは標準ライブラリの方がよかろう、と判断して使ってみる。ところがこいつに関数を渡そうとしてもうまく動かない。
template<typename T1, typename T2> inline std::vector<typename std::result_of<decltype(add<T1, T2>(T1, T2))>::type> //error! operator+(const std::vector<T1>& lhs, const std::vector<T2>& rhs) template<typename T1, typename T2> inline std::vector<typename std::result_of<add<T1, T2>>::type> //error! operator+(const std::vector<T1>& lhs, const std::vector<T2>& rhs)
このあたりはまだ少し良くわかっていない。実装はtype_traits
ヘッダにあるもののそう簡単にわかるものではなかった。add<T1, T2>(args...)
なる式の型を取り出すのに手こずっているということはわかるのだが……。
試しつつstd::result_of
に関して例えばこの辺の記事を読んだ。
- std::result_ofメタ関数のテンプレートパラメータ - yohhoyの日記
- decltypeとresult_ofの違い - Faith and Brave - C++で遊ぼう
- そろそろ result_of について - iorate's blog
もちろん、add
を関数オブジェクトにすることによってこの問題は回避できる。素の関数のままにしておきたいなら、std::function
とかでラップすると可能になると思われるのだが(最初にやっていたものに近い)。
と、いうことで一件落着なのだが、よく考えると
template<typename T1, typename T2> auto operator+(std::vector<T1> const& lhs, std::vector<T2> const& rhs) -> std::vector<decltype(lhs.front() + rhs.front())> { // impl. }
で良いのでは、という考えが去来した。さてこの場合、lhs
が空とかそういうことは問題になるのだろうか。この式の評価(?)はコンパイル時の型の解決のタイミングで行われそうで、std::vector::front()
の戻り値型がわかっている以上、その処理はそこで完結する。実際に、というと変だが、実行時にそこに書かれた式が評価されることはないので、実行時にlhs
が空だったらとかそういう俗世のことは絡んで来なさそうに思える。この式が実行時に評価されることはない……はずだ、と直感的には思うものの、確信が持てない。
実際、
戻り値の型を後置する関数宣言構文 - cpprefjp C++日本語リファレンス
にあるコードには、『decltype((*(T*)0)+(*(U*)0))
』なるものが出てくるが、これはnullptr
のC-style cast
にしか見えない。今風に書けばdecltype(*static_cast<T*>(nullptr) + *static_cast<U*>(nullptr))
になるだろう。こんなものを見るとnullptr
をデリファレンスしている時点でぶっ倒れそうになる。これが合法なのかはまだC++歴1年半・プログラミング歴2年半の若輩である私には分からないので、ちゃんと規格書かC++の人外に尋ねるべきな気もするが、とりあえずコンパイルは通った。偶然動いただけの未定義動作や処理系定義の可能性があるので、何の保証にもならないが。また規格書を読むべき項目のリストが増えた。積み上がり方に対して消化速度が追いつく気配がない。
となれば、decltype(lhs.front() + rhs.front())
だろうが何だろうが通りそうな気がするし、それ以前にdecltype(*static_cast<T1*>(nullptr) + *static_cast<T2*>(nullptr))
で良いのではという気がしてきた。何とはなしにそのままresult_of
の説明などを読んでいると、見つけた、標準にstd::declval
なるものがあるではないか。説明を読む限り、「実際には評価されない式においてある型の値を取り出すために用いる」らしいので、まさにこのような場合のために用意されたもののように見える。
declval - cpprefjp C++日本語リファレンス
これを使うと、
template<typename T1, typename T2> std::vector<decltype(std::declval<T1>() + std::declval<T2>())> operator+(std::vector<T1> const& lhs, std::vector<T2> const& rhs) { // impl. }
なんとまあスッキリするものか。declval
によってその式が何のためにあるのかがわかりやすくなっている。
ところでこれは前方宣言なしでは2重std::vector
の時にコンパイルエラーを起こしたりしたが、単に前方宣言をつけてやると動いた。
というわけで任意の次元の加算可能な要素を持つvector
に対する加算がかなりスッキリ書けるようになった。これは全部std::transformでいい気がする。