[C++]std::make_tuple、std::tie、std::forward_as_tupleの違い。

 いずれも引数を纏めてstd::tupleを作成するという意味では共通している。ただしこれら3つの振る舞いはいずれも異なっていて、状況に応じて適切に使い分ける必要がある。簡潔に言えば、std::make_tupleは引数のコピーを作り、std::tieは引数の左辺値参照を作り、std::forward_as_tupleは引数を纏めて転送する。

std::make_tuple

 基本的には引数のコピーを格納したstd::tupleを作る。与えられた引数が右辺値か左辺値かconstかなどに関わらず、その参照を取っ払ってコピーするのである。したがって引数に与えた変数と作られたstd::tupleが持つ要素は互いに独立したものになる。ただしstd::reference_wrapperを使うことで、明示的に参照として保持するよう指示することは出来る。

int x = 3;
double y = 0.14;
std::string z  ="0.001592"
auto t1 = std::make_tuple(x, y, z);//t1はstd::tuple<int, double, std::string>
auto t2 = std::make_tuple(x, std::cref(y), z);//t2はstd::tuple<int, const double&, std::string>

std::tie

 変数の型に関わらず、全て左辺値参照として保持する。つまり引数と作られたstd::tupleの間で値が共有され、std::tupleの方に値を代入すれば元の引数も変化するようになる。
 std::tupleに押し込まれた複数の戻り値を返す関数に対して、それらを分解して各々別の変数として受け取る場合に使うことが多い。のだが、私はほとんど使ったことがない。だいたいstd::getで十分であるし、C++17で構造化束縛が登場したことにより余計に立場を失ったような印象がある。構造化束縛と違い宣言済みの変数にしか代入できないので、基本的に参照型の変数に対して使えないし、デフォルトコンストラクタを持たないインスタンスに代入したい場合も厄介だ。
 ただし構造化束縛は逆に宣言済みの変数に代入することができないので、std::tieの出番がまるっきり失われたわけではない。

std::tuple<int, double, std::string> func() { return std::make_tuple(3, 0.14, "0.001592"); }
int x = 0;
double y = 0;
std::string z  ="0";
std::tie(x, y, z) = func();
//std::tieの戻り値はstd::tuple<int&, double&, std::string&>型なので、
//funcの戻り値が分解されx、y、z各変数に代入される。
//C++17ならauto[x, y, z] = func();の方が簡単。

printf("x == %d, y == %.2lf, z == %s\n", x, y, z.c_str());
//x == 3、y == 0.14, z == "0.001592"

std::forward_as_tuple

 std::forwardのように、左辺値を左辺値参照として、右辺値を右辺値参照としてstd::tupleに纏める。したがってコピーされない。
 それだけではこの関数の有意性を見いだせないかもしれないが、この関数が真価を発揮するのは複雑な可変長引数関数テンプレートを扱うときだ。std::forward_as_tupleの強みは、可変長引数テンプレートによってパックされた引数を、単一のstd::tupleに押し込みつつ完全転送できることにある。テンプレートライブラリを書いている私の場合、std::make_tupleやstd::tieよりも出番が多い。この完全転送を利用してstd::mapなどのemplace関数にも使われている。
 可変長引数テンプレートや完全転送を正しく理解していないと使いこなせないので、まずそちらを勉強すべし。

std::map<std::string, std::vector<double>> m;
m.emplace(std::piecewise_construct, std::forward_as_tuple("abcde"), std::forward_as_tuple(5, 1.0));
//emplace("abcde", 5, 1.0)と呼び出してしまうと、
//どこまでがキーのコンストラクタのための引数で、どこからが値のための引数なのかが分からない。
//よって、std::forward_as_tupleによって引数を纏めることで区別している。
//このとき、std::forward_as_tupleによって作られるstd::tupleの要素は、
//左辺値参照か右辺値参照のどちらか適切に判断されて構築される。つまり、完全転送される。

 C++を覚えて1~2年の頃は、こんな初歩的なところも理解できずに悩んだりしたものだ。ググってみても解説らしい解説がなかったので、ちょっと書き留めておく。