前回、SIMDベクトルライブラリを作った話をしたが、そこでやっていることについてかいつまんで書いておく。
まず、以下のようなクラスがある。
namespace mave { template<typename T, std::size_t R, std::size_t C> class matrix; template<typename T, std::size_t N> using vector = matrix<T, N, 1>; }
これについて、ごく普通の演算子の他に、以下のような演算子オーバーロードを行った。
namespace mave { template<typename T, std::size_t N> std::tuple<vector<T, N>, vector<T, N>> operator+(std::tuple<const vector<T, N>&, const vector<T, N>&> lhs, std::tuple<const vector<T, N>&, const vector<T, N>&> rhs) template<typename T, std::size_t N> std::tuple<vector<T, N>, vector<T, N>> operator+=(std::tuple< vector<T, N>&, vector<T, N>&> lhs, std::tuple<const vector<T, N>&, const vector<T, N>&> rhs) }
そう、あまり見る機会はないが、実はoperator+=
もクラス外で定義できる。通常この演算子はメンバへの書き込みを伴うのでクラス内でメンバとして定義する(ことが多い)。しかし今回は、std::tuple
にはメンバを追加できないのでfreeな関数として実装している。まあこの場合はコンストラクタで与えられるものが全てなので、メンバ関数として実装する必要がそもそもないのだが。
念頭にあるのは以下のようなユーザーコードである。
const mave::vector<float, 3> v1(/*...*/), v2(/*...*/); const mave::vector<float, 3> w1(/*...*/), w2(/*...*/); auto [u1, u2] = std::tie(v1, v2) + std::tie(w1, w2);
まず、std::tie
は参照を格納したstd::tuple
を作って返す。これは本来、タプルのパターンマッチをライブラリレベルでサポートするための機能だ。普通は以下のように使う。
std::tuple<int, double, std::string> f(); int main() { int i; double d; std::string s; std::tie(i, d, s) = f(); }
これは、多値を返す関数の戻り値を分解しつつ受け取るコードだ。何が起きるかというと、まずstd::tie
によってstd::tuple<int&, double&, std::string&>
が作られ、そこにstd::tuple<int, double, std::string>
が代入される。参照への代入なのでもともとのオブジェクトへの書き込みが行われ、i
、d
、s
のそれぞれが書き換わるという寸法だ。その後std::tie
によって作成されたオブジェクト(のprvalue
)の寿命は人知れず切れる。
ついでにこれはstd::ignore
を使うことで一部の値を無視することができる。
int i; double d; std::tie(i, d, std::ignore) = f();
このstd::ignore
は処理系定義なクラスのインスタンスと定義されている。代入演算子をtemplate
化してどんな型でも代入できるようにしておき、実際には代入演算子は何もしないようにすれば実装できるだろう。こんな感じで。
struct ignore_t { template<typename T> ignore_t& operator=(const T&) noexcept {return *this;} };
これらの機能は便利だったのだが、std::tie
だと初期化にコストのかかるオブジェクトを受け取りたい時に不必要なコストがかかってしまうことがある(先にデフォルトコンストラクタが走り、その後std::tie
で受け取るときムーブコンストラクタが走る)。また、そもそもデフォルトコンストラクタを持っていないとstd::tie
は使いにくい。適当な引数を与えてあとで代入することで使えないわけではないが、明らかに意味不明だろう。というわけで、結局コア言語側で構造化束縛が導入されることになった。
閑話休題、std::tie
は参照のタプルを作るので、参照のタプルを受け取って使うことにすればよい、というのは使い方を決めると自動的に決まる。要素数が2個の場合はstd::pair
を使えるようにしようかと一瞬思ったが、微妙に複雑になってしまう(結局tuple
もサポートしないといけないので転送するだけの関数が増えてしまう)のでやめた。
さて、これで大丈夫だろう、と思ったのだが、少し困ったことが起きた。先ほどの状態でこれは通ったのだが、
const mave::vector<float, 3> v1(/*...*/), v2(/*...*/); const mave::vector<float, 3> w1(/*...*/), w2(/*...*/); auto [u1, u2] = std::tie(v1, v2) + std::tie(w1, w2);
以下は通らなかった。
mave::vector<float, 3> v1(/*...*/), v2(/*...*/); mave::vector<float, 3> w1(/*...*/), w2(/*...*/); auto [u1, u2] = std::tie(v1, v2) + std::tie(w1, w2);
例えば、以下のようなtemplate
関数があったとする。
template<typename T1, typename T2> void f(std::tuple<T1 const&, T2 const&> tied) { std::cout << std::get<0>(tied) << ", " << std::get<1>(tied) << std::endl; }
これにstd::tuple<int&, double&>
などを渡すと、const T1
とint
がマッチしないのでこれは候補にならない。なので、以下は通らない。
int i = 42; double pi = 3.14; f(std::tie(i, pi)); // no matching function!
std::tie(T1, T2)
はstd::tuple<T1&, T2&>
を返すからだ。だが、もちろんT&
はconst T&
に変換できるので、template
をやめると通る。
void f(std::tuple<int const&, double const&> tied) { std::cout << std::get<0>(tied) << ", " << std::get<1>(tied) << std::endl; } int main() { int i = 42; double pi = 3.14; f(std::tie(i, pi)); // conversion from int& to int const& return 0; }
これが通ってtemplate
にすると通らないのはハマりどころな気がする。実際、書いてエラーを見るまでtemplate
でも通ると思っていた。一度、呼び出す関数の解決順序を見直した方が良さそうだ。
さて、これをどうするかだ。多分template
をやめてmave::vector<float, 3>
、<double,3>
、matrix<float, 3, 3>
……とそれぞれに演算子オーバーロードを用意すれば良いのだが、その場合用意していないサイズのmatrix
は、作れるが演算だけできないというよくわからないことになる。全てのサイズ、型についてオーバーロードを用意することはできない。そういう問題を回避する為にtemplate
があるのだ。では非template
とtemplate
演算子を共存させれば、と考えるわけだが、当然template
版と非template
版の両方がマッチするので、conflictしてオーバーロード解決が失敗する。
解決策として、こうした。
#define MAVE_GENERATE_FORWARDING_BINARY_FUNCTIONS_MATRIX_MATRIX_MATRIX(FUNC_NAME, L_MODIFICATION, R_MODIFICATION)\ //... MAVE_GENERATE_FORWARDING_BINARY_FUNCTIONS_MATRIX_MATRIX_MATRIX(operator+, MAVE_EMPTY_ARGUMENT, MAVE_EMPTY_ARGUMENT) MAVE_GENERATE_FORWARDING_BINARY_FUNCTIONS_MATRIX_MATRIX_MATRIX(operator+, const& , MAVE_EMPTY_ARGUMENT) MAVE_GENERATE_FORWARDING_BINARY_FUNCTIONS_MATRIX_MATRIX_MATRIX(operator+, & , MAVE_EMPTY_ARGUMENT) MAVE_GENERATE_FORWARDING_BINARY_FUNCTIONS_MATRIX_MATRIX_MATRIX(operator+, MAVE_EMPTY_ARGUMENT, &) MAVE_GENERATE_FORWARDING_BINARY_FUNCTIONS_MATRIX_MATRIX_MATRIX(operator+, const& , &) MAVE_GENERATE_FORWARDING_BINARY_FUNCTIONS_MATRIX_MATRIX_MATRIX(operator+, & , &) MAVE_GENERATE_FORWARDING_BINARY_FUNCTIONS_MATRIX_MATRIX_MATRIX(operator+, MAVE_EMPTY_ARGUMENT, const&) MAVE_GENERATE_FORWARDING_BINARY_FUNCTIONS_MATRIX_MATRIX_MATRIX(operator+, & , const&)
グワーッ!
渡されうる引数はconst&
、&
、(値渡し)の3通りがありえて、2引数取る場合はその組み合わせで増えるので大変なことになる。
これ、何かいい解決法がないだろうか。考えているのだが、浮かばなくて困っている。