[C++]std::tupleのコンストラクタとstd::anyに関する豆知識。

問題

次のコードを実行するとどうなるか?

#include <iostream>
#include <cstdlib>
#include <tuple>
#include <any>

void print(const std::any& a)
{
    if (a.type() == typeid(int)) std::cout << "any contains int" << std::endl;
    else if (a.type() == typeid(std::tuple<int>)) std::cout << "any contains std::tuple<int>" << std::endl;
}
int main()
{
    std::tuple<std::any> a = 5;//(1)
    print(std::get<0>(a));

    std::tuple<std::any> b = std::make_any<int>(5);//(2)
    print(std::get<0>(b));

    std::tuple<std::any> c = std::make_tuple(5);//(3)
    print(std::get<0>(c));

    std::tuple<std::any> d = std::make_tuple(std::make_any<int>(5));//(4)
    print(std::get<0>(d));

    return 0;
}

答え

any contains int//(1)
any contains int//(2)
any contains std::tuple<int>//(3)
any contains int//(4)

(1)、(2)はまあ問題ないだろう。(4)も直感的にはそりゃそうなるだろうなという感じ。 しかし(3)だけ想像と異なっていた。

解説?

私自身この挙動を把握しきれていないので正しいかどうか自信がないのだが、とりあえずMSVCのtupleの実装を読んだ上での現時点での理解は以下の通り。
std::tuple<Types...>のコンストラクタとしてよく使われるのは次のものである。

tuple(const Types& ...);        (a)
template <class ...UTypes>
tuple(UTypes&& ...);            (b)
template <class ...UTypes>
tuple(const std::tuple<UTypes...>&); (c)
template <class... UTypes>
tuple(tuple<UTypes...>&&);        (d)

先の4つのうち、(3)と(4)に絞って考えてみる。std::make_tupleの戻り値はrvalue扱いなので、呼び出しの候補となるのは(b)と(d)である。
std::anyは任意の型を格納することができるため、上記のうち(b)と(d)はどちらも引数的には呼び出し可能だ。
ただし要素が1個しかない場合、オーバーロードの解決のために(b)の呼び出しには条件がつく。引数が1個しかない場合、それがtuple<Types...>に一致しないことである1。したがって、(4)に限ればstd::make_tuple(std::make_any<int>(5))は戻り値がstd::tuple<std::any>に一致するため(d)のみが呼び出し候補となる。
では(3)の場合は?std::make_tuple(5)は当然ながらstd::tuple<int>であるため条件を満たしており、(b)の呼び出しが可能だ。では(d)とのオーバーロードをどうやって解決しているのかといえば、(d)にはまた別の条件が課されているのである。すなわち、TypesとUTypesが共に1個であるとき、Typesはstd::tuple<UTypes>から構築できないかつTypesはstd::tuple<UTypes>から変換できないの2つだ。困ったことにstd::anystd::tuple<int>から構築可能であるため、この条件に引っかかってしまう。結果、(b)のみが呼び出し候補となる。

発端

先日の型情報を保持する汎用参照と、関数テンプレートでなくてもできるジェネリクスもどきという記事を書いている時、std::anyに類似する自作機能が次のような感じのコードに引っかかってしまったのが事の始まり。

template <size_t Size>
using any_tuple = //std::tuple<std::any, ..., std::any>のように、std::anyをSize個連結したもの。

int main()
{
    any_tuple<1> a(std::make_tuple(1));//これだけstd::anyの中身がstd::tuple<int>になってしまう。
    any_tuple<2> b(std::make_tuple(1, 2));//引数が2個以上なら、std::anyの中身はちゃんとint型になる。
    any_tuple<3> c(std::make_tuple(1, 2, 3));
    any_tuple<4> d(std::make_tuple(1, 2, 3, 4));
}

結論から言えば、ちゃんとstd::make_anyしようぜ、という話なのだろう。tupleがいろんな型を勝手に変換してくれて便利だからといって、横着なコードを書いているとおかしなことになるよ、という戒め。


  1. 結論から言えば、私はこの条件を勘違いしていた。“引数がtuple<Types...>に一致しない”、ではなく、“引数がtupleではないか、tupleであってもその要素一つ一つからTypesを構築できない”だと思っていたのだ。