こんにちは。田原です。
私はChironianのハンドルで良くteratailに書き込みしているのですが、ちょと前にユニバーサル参照に関連した質問がありました。そこで私も初めて気がついたのですが、ユニバーサル参照には意外な落とし穴がありました。小さいのでハマることはまず無いとは思いますが、ハマると苦労しそうですので今回はこれを取り上げることにしました。
1.ユニバーサル参照の復習
関数テンプレートの型推論により、左辺値(変数など)と右辺値(一時オブジェクトなど)を変更可能な参照で受け取ることができる仕組みです。
// ユニバーサル参照で受け取る関数テンプレート template<typename T> void foo(T&& iParam) { }
のような T&& のことです。foo()関数を呼び出した時、実引数として左辺値(例えばローカル変数)を渡すとiParamは左辺値参照となります。実引数として右辺値(一時オブジェクトやstd::moveした変数)を渡すとiParamは右辺値参照となります。両者ともconstが付加されることはありませんので変更可能な状態で参照できます。
int main() { int data=123456; foo(data); // iParamはint& 型 foo(123456); // iParamはint&& 型 foo(std::move(data)); // 同上 }
右辺値参照とともにユニバーサル参照もC++11で追加されましたが、それ以前でも左辺値と右辺値の両方を受け取ることができる参照がありました。const参照です。参照先をconst修飾していますので変更できません。const参照で足りる時(参照先を変更しない時)はこちらを使っておいた方が安全です。
2.Tの型は?
ここまではstd::forwardにて解説した通りです。
さて、その際に解説しなかったことがあります。この時、Tはどんな型になるでしょうか?
上記の右辺値を受け取りiParamが右辺値参照(int&&)になる時はint型です。
問題は左辺値を受け取った時です。
2-1.良くあるケース
ユニバーサル参照ではない場合、Tは元の型になる事が多いです。
例えば、次のケースでは全て T はint型となります。(参照やポインタにはなりません。)
template<typename T> void foo(T&& iParam) { } template<typename T> void constRef(T const& iParam) { } template<typename T> void ref(T& iParam) { } template<typename T> void ptr(T* iParam) { } template<typename T> void constPtr(T const* iParam) { } int main() { int data=123456; foo(123456); foo(std::move(data)); constRef(data); constRef(123456); constRef(std::move(data)); ref(data); ptr(&data); constPtr(&data); }
2-2.ユニバーサル参照に左辺値を渡した時は?
前節の結果から、実引数として左辺値(変数等)を渡してiParamが左辺値参照(int&)になる時、Tはint型になると私は思い込んでいました。しかし、調べてみると実はそうではありませんでした。int型の参照(int&)となります。
前節のwandboxのサンプル・プログラムに以下の行を追加してみて下さい。
foo(data);
その行で次のように表示され、Tが int& になることを確認できます。
foo(123456) T = Type<int&> type of iParam = Type<int&>
3.その結果、落とし穴にハマることがあります
冒頭でリンクしたteratailで質問された方はハマっていましたが、私もいつかハマりそうです。
質問の内容はswap()関数テンプレートでしたが、質問と同じ例を取り扱っても面白くないですし、std::swap()は右辺値を受け取りません。そこで、ここ何回かtupleを例に取り上げていますので今回もtupleを対象とします。
std::make_tuple()関数テンプレートの簡易版を作ってみます。
3-1.脊髄反射版
私が最初に作るとしたら、次のようなものになりそうです。
template<typename... tTypes> tuple<tTypes...> make_my_tuple(tTypes&&... iParam) { return tuple<tTypes...>(iParam...); }
そして、次のようなコードでデバッグし、OKと考えることもあるかも知れません。
(流石にこのテストは甘すぎです。でもテンプレートのテストってキリがないので悩ましいですよね。)
// std::string型の型名が長くて判りにくいので短縮用 struct String : public std::string { using std::string::string; }; // メイン int main() { std::cout << "--- my tuple ---\n"; auto aTuple = make_my_tuple(123, 456, String("hello")); std::cout << get<0>(aTuple) << "\n"; std::cout << get<1>(aTuple) << "\n"; std::cout << get<2>(aTuple) << "\n"; get<2>(aTuple) = "world!!"; std::cout << get<2>(aTuple) << "\n"; std::cout << "type of aTuple = " << TYPENAME(aTuple) << "\n"; }
--- my tuple --- 123 456 hello world!! type of aTuple = tuple<int, int, String>
そして、安心して使っているとひどい目に会うかも知れません。
int main() { std::cout << "\n--- oops!!---\n"; int x = 123; int y = 456; String z("hello"); auto aTuple2 = make_my_tuple(x, y, z); std::cout << get<0>(aTuple2) << "\n"; std::cout << get<1>(aTuple2) << "\n"; std::cout << get<2>(aTuple2) << "\n"; get<2>(aTuple2) = "world!!"; std::cout << get<2>(aTuple2) << "\n"; std::cout << "z=" << z << "\n"; }
--- oops!!--- 123 456 hello world!! z=world!!
あらら、get<2>(aTuple2)に代入すると z まで変わってしまいました。get<2>(aTuple2)が変数zへの参照になってしまっているようです。コードを追加すると次のような出力となり、確かに参照になっていることを確認できます。
std::cout << "type of aTuple2 = " << TYPENAME(aTuple2) << "\n";
type of aTuple2 = tuple<int&, int&, String&>
3-2.本家std::make_tupleの場合
意図的に各要素を元の変数への参照にしたいケースもありますが、tuple内に別途メモリが確保されることを期待することの方が多いのではないでしょうか?(前者のケースがそのようになっています。)
実際、本家のstd::make_tuple定義をよ~く見てみて下さい。テンプレート・パラメータ・パック名はTypesですが、tuple<VTypes…>が戻り値です。 V がついてます!! もし、同じTypesのままだったら上記と同様の働きとなり変数でmake_tupleしたら参照になる筈です。
template <class... Types> tuple<VTypes ...> make_tuple(Types&&...);
このVTypesを含む戻り値の説明は複雑ですが、ざっくり書くとTypesの各型Tに対応するVTypesの各型Uは下記のどちらかになるということです。
- 原則として、Uは
std::decay<T>
::typeである。
std::decay(結構複雑です)の今回のケースと密接なものは「T が参照型なら std::remove_reference<T>
する(参照を外す)」機能です。 -
ただし、T が
std::reference_wrapper<U>
型なら、U&型
つまり、渡した要素を参照したい時は「実引数をstd::reference_wrapper<T>
へキャストしなさい」ということです。そのための関数としてstd::ref<T>
が用意されています。
本家std::make_tupleは、変数を渡してもstd::remove_referece<T>
により参照ではなくなり変数が定義されるため、元の変数とは別の領域が確保されます。
// メイン int main() { std::cout << "\n--- std::tuple ---\n"; int x = 123; int y = 456; String z("hello"); auto aStdTuple = std::make_tuple(x, y, z); std::cout << std::get<0>(aStdTuple) << std::endl; std::cout << std::get<1>(aStdTuple) << std::endl; std::cout << std::get<2>(aStdTuple) << std::endl; std::get<2>(aStdTuple) = "world!!"; std::cout << std::get<2>(aStdTuple) << "\n"; std::cout << "z=" << z << "\n"; std::cout << "type of aStdTuple = " << TYPENAME(aStdTuple) << "\n"; }
--- std::tuple --- 123 456 hello world!! z=hello type of aStdTuple = std::tuple<int, int, String>
3-3.もう少しまともな版
上述の機能を真面目に「make_my_tuple」に反映するのは面倒ですので、実引数へ左辺値(変数)を渡した時に、型に付いてしまう参照を外す機能のみ対応してみました。
template<typename... tTypes> tuple<typename std::remove_reference<tTypes>::type...> make_my_tuple(tTypes&&... iParam) { return tuple<typename std::remove_reference<tTypes>::type...>(iParam...); }
結果は次のようになり、無事に z が独立した変数となりました。
123 456 hello world!! z=hello type of aTuple2 = tuple<int, int, String>
4.まとめ
ユニバーサル参照で受ける時はstd::forwardで中継することが多いと思いますし、今回サンプルを見つけるのもちょっと苦労しました。ですので、この問題に出会うことはあまりないと思います。それだけに出会った時はハマりそうでちょっと怖いです。
ユニバーサル参照で受けた時、関数パラメータの型は素直(左辺値は左辺値参照、右辺値は右辺値参照)です。しかし、テンプレート・パラメータにはちょっと意外な型が渡るので要注意であることを覚えていればなんとかなるかも知れません。
さて、前回「ネタが尽きてきた」と書いたら、いなむ先生からCRTP(Curiously Recurring Template Pattern; 奇妙に再帰したテンプレートパターン)はどうですか?とのお勧めを頂きました。
CRTPはメンバ関数の自動増殖などに便利なデザイン・パターンです。また難易度も適度ですので、次回CRTPを解説します。お楽しみに!