Initializing smart pointers in a tuple without knowing the type

443 views Asked by At

I have a tuple of smart pointers (as a member of a class template) that I need to initialize. I use std::apply to iterate over tuples elsewhere, but how do I initialize them with new objects without knowing their type? Running the code below with a debugger tells me the elements in the tuple are still "empty" afterwards. What am I doing wrong here?

struct A {
  int a = 1;
}

struct B {
  int b = 2;
}

std::tuple< std::unique_ptr< A >, std::unique_ptr< B > > my_tuple;

std::apply( [] ( auto&... ptr ) { //std::unique_ptr< T >
  (..., ptr.reset( { } )); //ptr.reset( new T() )
}, my_tuple );
3

There are 3 answers

1
HolyBlackCat On BEST ANSWER

As noted in the comments, you can apply decltype to ptr to get the type of the unique_ptr, then apply element_type to it:

std::apply([](auto &... ptr)
{
    ((ptr = std::make_unique<typename std::remove_reference_t<decltype(ptr)>::element_type>()), ...);
}, my_tuple );

(I've replaced new with make_unique, and moved ... to the end, but those are just style clanges.)

This can be shortened with C++20 template lambdas:

std::apply([]<typename ...P>(std::unique_ptr<P> &...ptrs )
{
    ((ptrs = std::make_unique<P>()), ...);
}, my_tuple );
0
Ted Lyngmo On

I'm not 100% sure of what use-cases you want to support so I made a few function templates that may suit what you are trying to do.

#include <memory>
#include <tuple>
#include <type_traits>

// create a tuple of unique_ptr's pointing to default constructed objects
template<class... Ts>
std::tuple<std::unique_ptr<Ts>...> init_up_tuple() {
    return std::make_tuple(std::make_unique<Ts>()...);
}

// create a tuple of unique_ptr's with copies of the supplied objects
template<class... Ts>
std::tuple<std::unique_ptr<Ts>...> init_up_tuple(const Ts&... ts) {
    return std::make_tuple(std::make_unique<Ts>(ts)...);
}

// create a tuple of unique_ptr's with default constructed objects of the same
// types as those in the supplied tuple
template<class... Ts>
std::tuple<std::unique_ptr<Ts>...> init_up_tuple(
    const std::tuple<std::unique_ptr<Ts>...>&)
{
    return std::make_tuple(std::make_unique<Ts>()...);
}

namespace detail {
template<class... Ts, size_t... Idx>
std::tuple<std::unique_ptr<Ts>...> copy_up_tuple_impl(
    const std::tuple<std::unique_ptr<Ts>...>& tu,
    std::index_sequence<Idx...>)
{
    return std::make_tuple(std::make_unique<Ts>(*std::get<Idx>(tu))...);
}
} // namespace detail

// create a tuple of unique_ptr's pointing to copy constructed objects
// from the objects in the supplied tuple of unique_ptr's
template<class... Ts, typename Indices = std::index_sequence_for<Ts...>>
std::tuple<std::unique_ptr<Ts>...> copy_up_tuple(
    const std::tuple<std::unique_ptr<Ts>...>& tu)
{
    return detail::copy_up_tuple_impl(tu, Indices{});
}

These could then be used like this:

#include <iostream>

struct A {
    int a = 1;
};

struct B {
    int b = 2;
};

int main() {
    auto t1 = init_up_tuple<A, B>();  // default constructed
    std::cout << std::get<0>(t1)->a << ' ' << std::get<1>(t1)->b << '\n';

    A a{3};
    auto t2 = init_up_tuple(a, B{4}); // copy construct from the supplied objects
    std::cout << std::get<0>(t2)->a << ' ' << std::get<1>(t2)->b << '\n';

    auto t3 = copy_up_tuple(t2);      // copy construct from tuple of unique_ptr's
    std::cout << std::get<0>(t3)->a << ' ' << std::get<1>(t3)->b << '\n';

    t3 = init_up_tuple(t3);           // reset to default
    std::cout << std::get<0>(t3)->a << ' ' << std::get<1>(t3)->b << '\n';
}

Output:

1 2
3 4
3 4
1 2

Demo

0
jwezorek On

If you just want to reset the unique pointers to the default for the types, you can iterate over the values in a tuple using numerical index like below

#include <tuple>
#include <memory>
#include <iostream>

struct A {
    int u;
    A() : u(5) {};
};

struct B {
    int v;
    B() : v(6) {};
};

template<size_t I = 0, typename... Tp>
void reset_tuple_of_unique_ptrs(std::tuple<Tp...>& t) {

    auto& item = std::get<I>(t);
    using T = typename std::remove_reference_t<decltype(item)>::element_type;

    item.reset( new T() );
    if constexpr (I + 1 != sizeof...(Tp))
        reset_tuple_of_unique_ptrs<I + 1>(t);
}

int main() {
    std::tuple<std::unique_ptr<A>, std::unique_ptr<B>> tup = { std::make_unique<A>(), std::make_unique<B>() };
    std::get<0>(tup)->u = 42;
    std::get<1>(tup)->v = 17;

    std::cout << std::get<0>(tup)->u << " , " << std::get<1>(tup)->v << "\n";
    
    reset_tuple_of_unique_ptrs(tup);

    std::cout << std::get<0>(tup)->u << " , " << std::get<1>(tup)->v << "\n";
}