sayurc
sayurc

Reputation: 536

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

Upvotes: 8

Views: 450

Answers (2)

Nicol Bolas
Nicol Bolas

Reputation: 474266

tuple has a bunch of constructors which can overlap in their meaning. There are priorities between them as to when which one gets called.

But a core rule of C++ is this: if you have a value of type T, and you create a new type of exactly the type T from that value, that process will invoke the copy (or move) constructor.

Your first example does that: t1 and t2 are the same type, so a copy happens.

Your second example does not. t2 is a completely different type from t1. Therefore, the copy constructor doesn't get used; instead various tuple constructors are considered.

There is a tuple constructor which allows creating a tuple<Ts> from a tuple<Us>, so long as Ts and Us have the same number of types and that each T is implicitly convertible from the corresponding U.

However... that isn't the case here. std::any is not implicitly convertible from an int (or most types). So this constructor is not considered.

So what is considered is the construction of each element of the tuple from each parameter in the constructor call. Since you're using direct-list-initialization, explicit cosntructors are considered. And since there is an explicit conversion from std::tuple<int> (the type of the first parameter) to the type of the first member of the tuple (ie: std::any), that's what gets used.

How can I get around this issue?

When playing with any, you have to be careful of explicit conversions. If you want to store an int inside the any inside a tuple, you need to make this unambiguous. Pass exactly an int explicitly by geting the int out of t1.

Upvotes: 4

Dominik Kaszewski
Dominik Kaszewski

Reputation: 545

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);

Upvotes: 6

Related Questions