Search code examples
c++c++20stdtuplestdany

Inconsistency in the Constructors of `std::tuple` When Using `std::any` Elements


This code works as expected:

#include <iostream>
#include <tuple>

int main() {
    std::tuple<int> t1{3};
    std::tuple<int> t2{t1};

    std::cout << std::get<0>(t2) << std::endl;

    return 0;
}

It correctly prints the number 3. But this code doesn’t:

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

int main() {
    std::tuple<int> t1{3};
    std::tuple<std::any> t2{t1};

    std::cout << std::any_cast<int>(std::get<0>(t2)) << std::endl;

    return 0;
}

std::any_cast<int> throws the exception std::bad_any_cast. However, if I add one more element to the tuples it does work:

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

int main() {
    std::tuple<int, float> t1{3, 3.14};
    std::tuple<std::any, std::any> t2{t1};

    std::cout << std::any_cast<int>(std::get<0>(t2)) << std::endl;

    return 0;
}

I compiled everything with C++23.

I inspected the type of std::get<0>(t2):

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

int main() {
    std::tuple<int> t1{3};
    std::tuple<std::any> t2{t1};

    std::cout << std::get<0>(t2).type().name() << std::endl;

    return 0;
}

Compiling with Clang on Linux gave me the output St5tupleIJiEE, which the LLVM demangler shows as std::tuple<int>. So what is happening is that the tuple t1 is being copied to the std::any inside t2, while what I expected was the tuple itself t1 to be copied and the int going into std::any. This only happens when using a unary tuple with std::any, otherwise the behavior is as expected.

The cppreference page for the constructors of std::tuple has the following key constructors:

tuple( const Types&... args ); // (2)

template< class... UTypes >
constexpr tuple( tuple<UTypes...>& other ); // (4)

template< class... UTypes >
tuple( const tuple<UTypes...>& other ); // (5)

It seems that the unary tuple t2 is being constructed with (2) instead of (4) or (5), while the tuple with two elements is being constructed with (4) or (5).

So I have two questions

  • Is this the expected behavior or is it a bug in implementations of the STL? I linked to both stdlibc++ and libc++ and both failed.
  • How can I get around this issue? It works when I first declare t2 and then use the assignment operator to copy t1 into it, but I don’t want to do that since I might want to use std::move for performance but the code fails even in this case.

Solution

  • As @NathanOliver comments, you are trying to use converting operator (5), which requires std::tuple_size > 1.

    You can work around this issue by constructing the second tuple from the first's argument: std::tuple<std::any> t2{ std::get<0>(t1) };.

    If you are working in some generic code which needs to handle tuple with any number of std::any, then you need a slightly unwieldy:

    auto t2 = std::apply([](auto&& args) {
        return std::make_tuple(std::any{ std::forward<decltype(args)>(args) }...);
    }, t1);