std::tuple_cat but only with unique elements
Asked Answered
S

1

10

I have a need for a constexpr function that is very similar to std::tuple_cat, but instead of merging all elements regardless of what they are into one tuple, I need it to add a given element only if that type hasn't already been added.

Passing a predicate into std::tuple_cat would have been nice, but no such API exists (much to my sadness). I've seen a couple of ways to find a merged type using type traits that I haven't quite been able to fully understand, but nothing in the form of a constexpr function. I'm not sure how to put it all together although I am sure it can be done.

Something like this:

std::tuple<int, short, char> First;
std::tuple<short, float> Second;
std::tuple<int, double, short> Third;

std::tuple<int, short, char, float, double> Result = tuple_cat_unique(First,Second,Third);
Sapodilla answered 14/6, 2019 at 5:12 Comment(2)
If a type occurs more than one time, does the first one take precedence?Darladarlan
In this case, yes.Sapodilla
E
11

Possible solution:

template<std::size_t i, class Tuple, std::size_t... is>
constexpr auto element_as_tuple(const Tuple& tuple, std::index_sequence<is...>)
{
    if constexpr (!(std::is_same_v<std::tuple_element_t<i, Tuple>, 
                  std::tuple_element_t<is, Tuple>> || ...))
        return std::make_tuple(std::get<i>(tuple));
    else
        return std::make_tuple();
}

template<class Tuple, std::size_t... is>
constexpr auto make_tuple_unique(const Tuple& tuple, std::index_sequence<is...>)
{
    return std::tuple_cat(element_as_tuple<is>(tuple, 
                          std::make_index_sequence<is>{})...);
}

template<class... Tuples>
constexpr auto make_tuple_unique(const Tuples&... tuples)
{
    auto all = std::tuple_cat(tuples...);
    constexpr auto size = std::tuple_size_v<decltype(all)>;
    return make_tuple_unique(all, std::make_index_sequence<size>{});
}

constexpr std::tuple<int, short, char> first(1, 2, 3);
constexpr std::tuple<short, float> second(4, 5);
constexpr std::tuple<int, double, short> third(6, 7, 8);
constexpr auto t = make_tuple_unique(first, second, third);
static_assert(std::get<0>(t) == 1);
static_assert(std::get<1>(t) == 2);
static_assert(std::get<2>(t) == 3);
static_assert(std::get<3>(t) == 5);
static_assert(std::get<4>(t) == 7);

Generalization that will work also with movable-only types:

template<std::size_t i, class Tuple, std::size_t... is>
constexpr auto element_as_tuple(Tuple&& tuple, std::index_sequence<is...>)
{
    using T = std::remove_reference_t<Tuple>;
    if constexpr (!(std::is_same_v<std::tuple_element_t<i, T>, 
                  std::tuple_element_t<is, T>> || ...))         
        // see below
        // return std::forward_as_tuple(std::get<i>(std::forward<Tuple>(tuple)));
        return std::tuple<std::tuple_element_t<i, T>>(
            std::get<i>(std::forward<Tuple>(tuple)));
    else
        return std::make_tuple();
}

template<class Tuple, std::size_t... is>
constexpr auto make_tuple_unique(Tuple&& tuple, std::index_sequence<is...>)
{
    return std::tuple_cat(element_as_tuple<is>(std::forward<Tuple>(tuple), 
        std::make_index_sequence<is>())...);
}

template<class... Tuples>
constexpr auto make_tuple_unique(Tuples&&... tuples)
{
    auto all = std::tuple_cat(std::forward<Tuples>(tuples)...);
    return make_tuple_unique(std::move(all),
        std::make_index_sequence<std::tuple_size_v<decltype(all)>>{});
}

Addition/correction.

My initial testing showed it worked fine, but more in depth tests showed that using std::forward_as_tuple generates references to temporary variables (the "all" variable in make_tuple_unique). I had to change that the std::forward_as_tuple to std::make_tuple and everything was fixed.

That's correct: if you pass an rvalue as an argument, like

make_tuple_unique(std::tuple<int>(1))

the return type is std::tuple<int&&> and you get a dangling reference. But with std::make_tuple instead of std::forward_as_tuple

make_tuple_unique(std::tuple<int&>(i))

will have type std::tuple<int>, and a reference will be lost. With std::make_tuple we loose lvalues, with std::forward_as_tuple we loose plain values. To preserve the original type we should

return std::tuple<std::tuple_element_t<i, T>>(
    std::get<i>(std::forward<Tuple>(tuple)));
Edging answered 14/6, 2019 at 6:3 Comment(5)
Interesting approach - decompose into single-or-zero-element tuples, each one depending on type equality. Probably faster to compile than doing something SFINAE-ish with std::get's "Fails to compile unless the tuple has exactly one element of that type."...Henninger
@MaxLanghof, this is what I tried to do at the beginning. But it won't work: #41708991Edging
Brilliant solution and it solved my problem. Thanks!Sapodilla
Minor addendum for the move-friendly implementation. My initial testing showed it worked fine, but more in depth tests showed that using std::forward_as_tuple generates references to temporary variables (the "all" variable in make_tuple_unique). I had to change that the std::forward_as_tuple to std::make_tuple and everything was fixed.Sapodilla
@Mako_Energy, thanks for your addendum! I've updated the answer. Please check if the solution there works in your case. I've made simple checks, and it seems to be correct.Edging

© 2022 - 2024 — McMap. All rights reserved.