TL;DR
// 引数を1回しか使わない場合
template<typename X>
auto inverse(X&& x)
{
return 1 / std::forward<X>(x);
}
// 引数を複数回使う場合
template<typename X>
auto square(X&& x)
{
return X{x} * X{x};
}
xtensor
xtensor (GitHub) はC++14向けのヘッダオンリーテンソル代数ライブラリ。numpyライクなAPIとムーブセマンティクスをフル活用した設計が印象的です。
バージョン
- xtensor 0.16.2 (Homebrewから)
- Apple LLVM version 9.0.0 (clang-900.0.38)
問題: 式をそのまま関数にするとクラッシュする
xtensorは式テンプレートで遅延評価をします。条件によって式のオペランドが参照で保持されるので、適当に関数を書くと未定義動作になってしまいます。例えば、この $ (a + b)^2 $ を計算するコードで…
#include <iostream>
#include <xtensor/xio.hpp>
#include <xtensor/xtensor.hpp>
int main()
{
xt::xtensor<double, 3> a = {
{{1, 2, 3}, {4, 5, 6}},
{{7, 8, 9}, {0, 1, 2}},
};
xt::xtensor<double, 3> b = {
{{3, 4, 5}, {6, 7, 8}},
{{9, 0, 1}, {2, 3, 4}}
};
auto c = (a + b) * (a + b);
std::cout << c << '\n';
}
二乗の計算をそのまま関数化するとクラッシュします:
#include <iostream>
#include <xtensor/xio.hpp>
#include <xtensor/xtensor.hpp>
// NEW!!
template<typename X>
auto square(X x)
{
return x * x;
}
int main()
{
xt::xtensor<double, 3> a = {
{{1, 2, 3}, {4, 5, 6}},
{{7, 8, 9}, {0, 1, 2}},
};
xt::xtensor<double, 3> b = {
{{3, 4, 5}, {6, 7, 8}},
{{9, 0, 1}, {2, 3, 4}}
};
auto c = square(a + b);
std::cout << c << '\n';
}
$ ./main02
Segmentation fault: 11
このクラッシュは仮引数x
へのdangling referenceが原因です。
square()
関数中の式x * x
は、実際にはxt::xfunction<xt::detail::multiplies, ...>
という式テンプレートのオブジェクトを作っています。xtensorの式テンプレートは、オペランドが左辺値参照なら参照をそのまま保持、右辺値ならコピーを保持する仕様です (Closure Semantics)。ここでは仮引数x
への左辺値参照を渡しているため、main()
関数のc
はすでに破棄された仮引数x
を参照することになります。したがって、std::cout
でc
を使った時点でクラッシュしてしまいます。
引数を2回使っているからforwardできない
こういう場合、C++の定石では引数をuniversal referenceで受けてforwardするのですが…
template<typename X>
auto square(X&& x)
{
// BUG: 2回forwardしてはいけない
return std::forward<X>(x) * std::forward<X>(x);
}
これは多重forwardなのでNGです。引数がxt::xtensor
の右辺値だった場合、コンテナの二重moveとなりクラッシュします:
#include <iostream>
#include <utility>
#include <xtensor/xio.hpp>
#include <xtensor/xtensor.hpp>
template<typename X>
auto square(X&& x)
{
return std::forward<X>(x) * std::forward<X>(x);
}
int main()
{
std::cout << square(xt::xtensor<double, 1>{1, 2, 3}) << '\n';
}
$ ./main03
Segmentation fault: 11
このように二重moveは不可なので、引数に右辺値が渡されたらコピーを作る必要があります。
左辺値参照なら参照、右辺値ならコピーする
結局、引数はuniversal referenceで受けて、左辺値参照なら参照をそのまま、右辺値なら使うたびコピーして (新しい右辺値を作って) 使うのが安全です。C++のreference collapsingの仕様が下表のようになっているので、文法的にはX{x}
で実現できます。
引数 X&& | X | X{x} の意味 |
---|---|---|
左辺値参照 T& | T& | 参照 x |
右辺値参照 T&& | T | コピー T{x} |
これでちゃんと動く関数が作れました:
#include <iostream>
#include <xtensor/xio.hpp>
#include <xtensor/xtensor.hpp>
template<typename X>
auto square(X&& x)
{
return X{x} * X{x};
}
int main()
{
xt::xtensor<double, 3> a = {
{{1, 2, 3}, {4, 5, 6}},
{{7, 8, 9}, {0, 1, 2}},
};
xt::xtensor<double, 3> b = {
{{3, 4, 5}, {6, 7, 8}},
{{9, 0, 1}, {2, 3, 4}}
};
auto c = square(a + b);
std::cout << c << '\n';
std::cout << square(xt::xtensor<double, 1>{1, 2, 3}) << '\n';
}
$ ./main04
{{{ 16., 36., 64.},
{ 100., 144., 196.}},
{{ 256., 64., 100.},
{ 4., 16., 36.}}}
{ 1., 4., 9.}
ただし: 可能ならforwardすべき
引数を1回しか使わないのであれば、ドキュメント通りforwardすべきです:
template<typename X>
auto inverse(X&& x)
{
// xを1回しか使わないのでforward
return 1 / std::forward<X>(x);
}
ここでX{x}
と書いても正しく動作しますが、引数としてxt::xtensor<double, 2>{1, 2, 3}
のようなコンテナの右辺値が渡された場合にコンテナの無駄なコピーが発生してしまいます。std::forward<X>(x)
ならmoveされるので無駄がありません。
引数を複数回使う場合でも、最後の1回だけforwardすればコピーが1回だけ減らせそうです。が、C++17であっても掛け算のオペランドの評価順は保証されないそうなので (参考)、下のようには書けません。引数が左辺値参照の場合も考えだすともう全然分からないので、僕はあきらめました。
template<typename X>
auto square(X&& x)
{
// BUG: 順序の保証がない
return X{x} * std::forward<X>(x);
}
おわりに
xtensorがどんどん使われて記事が増えて、バグも潰れていったら嬉しいです。使う場合は参照まわりで未定義動作になりやすいので、Address Sanitizerを忘れずに。