Make integer sequence unique at compile time
Asked Answered
D

4

30

Suppose I have:

template<int... N>
class seq
{
};

template<int... N>
struct uniq{
    using type = seq<N...>;
};

I need to make the sequence unique somehow, so that

std::is_same_v<uniq<1,2,2,2,3,3,3>::type, seq<1, 2, 3>>; 

ends up being true. In other words make the sequence unique then create a seq.
Is there a way to achieve this at compile time?

Decemvirate answered 23/2, 2021 at 15:4 Comment(1)
Will your template parameters always be sorted?Shirleenshirlene
D
32

Using std

Using <type_traits> from the standard library, you can implement your own like this:

#include <type_traits>

namespace detail
{
template<class, auto... Ns>
struct uniq_impl;
template<template<auto...> class T, auto... Ms, auto N, auto... Ns>
struct uniq_impl<T<Ms...>, N, Ns...> : std::conditional_t<
    (... || (N == Ms)),
    uniq_impl<T<Ms...>, Ns...>,
    uniq_impl<T<Ms..., N>, Ns...>>
{
};
template<template<auto...> class T, auto... Ms>
struct uniq_impl<T<Ms...>>
{
    using type = T<Ms...>;
};
} // namespace detail

template<int... Ns>
class seq
{
};

template<int... Ns>
using uniq = detail::uniq_impl<seq<>, Ns...>;

static_assert(std::is_same_v<typename uniq<1,2,2,2,3,3,3>::type, seq<1, 2, 3>>);

uniq_impl works by starting with an empty seq<> and a parameter pack of auto... Ns, then taking the front of the parameter pack one at a time using the template specialization

template<template<auto...> class T, auto... Ms, auto N, auto... Ns>
struct uniq_impl<T<Ms...>, N, Ns...> : std::conditional_t<
    (... || (N == Ms)),
    uniq_impl<T<Ms...>, Ns...>,
    uniq_impl<T<Ms..., N>, Ns...>>
{
};

it checks whether N is in the set of auto... Ms using a fold expression and decides whether to push N onto Ms or discard it using std::conditional_t. Once auto... Ns is empty, it then uses the specialization

template<template<auto...> class T, auto... Ms>
struct uniq_impl<T<Ms...>>
{
    using type = T<Ms...>;
};

to tag the resulting container of unique values. Try it on godbolt.org: Demo.

Using boost::mp11

As others have pointed out, you can delegate the algorithm to boost::mp11::mp_unique, but because it works for types and not values, you'll need to wrap and unwrap the values to and from std::integral_constant in order to use this approach:

#include <boost/mp11/algorithm.hpp>

namespace detail
{
template<template<auto...> class T, auto... Ns>
class uniq_impl
{
    static boost::mp11::mp_list<std::integral_constant<decltype(Ns), Ns>...> types();

    template <class L>
    static boost::mp11::mp_unique<L> transform(L);

    template<class... Ts, auto... Ms>
    static T<Ms...> values(boost::mp11::mp_list<std::integral_constant<Ts, Ms>...>);

public:
    using type = decltype(values(transform(types()))); 
};
} // namespace detail

template<int... Ns>
class seq
{
};

template<int... Ns>
using uniq = detail::uniq_impl<seq, Ns...>;

static_assert(std::is_same_v<typename uniq<1,2,2,2,3,3,3>::type, seq<1, 2, 3>>);

Try it on godbolt.org: Demo.

Djokjakarta answered 23/2, 2021 at 16:8 Comment(1)
That is a very succinct and expressive example of some very pretty meta-programming. And it works for unsorted integer sequences too to boot.Lunneta
G
8

You can use boost::mp11::mp_unique for this.

Example:

#include <boost/mp11.hpp>

namespace
{
template <int... N>
using seq = boost::mp11::mp_list_c<int, N...>;

template <int... N>
struct uniq
{
    using type = boost::mp11::mp_unique<seq<N...>>;
};
}

int main()
{
    static_assert(std::is_same_v<uniq<1,2,2,2,3,3,3>::type, seq<1,2,3>>);
    static_assert(std::is_same_v<uniq<4,1,9,9,2,2,3,1,5>::type, seq<4,1,9,2,3,5>>);
    return 0;
}

If an alias isn't suitable for seq, you can do something like this:

template <int... N>
struct seq
{};

template <int... N>
struct uniq
{
private:
    template <int... Is>
    static constexpr auto uniquer(boost::mp11::mp_list_c<int, Is...>) -> seq<Is...>;

public:
    using type = decltype(uniquer(boost::mp11::mp_unique<boost::mp11::mp_list_c<int, N...>>{}));
};
Gustie answered 23/2, 2021 at 15:16 Comment(3)
I think this works for types not integer sequences. godbolt.org/z/7dd5zjStonecutter
@MarekR integer sequences can be types. I'll update my answer.Gustie
@PatrickRoberts you're right, it gets a little more complicated - I've updated my answer.Gustie
O
4

To remove adjacent duplicates (as for std::unique), you might do:

template <typename Seq, typename Res = std::index_sequence<>>
struct reverse;

template <typename Res>
struct reverse<std::index_sequence<>, Res>
{
    using type = Res;
};

template <std::size_t I, std::size_t ... Is, std::size_t ... Js>
struct reverse<std::index_sequence<I, Is...>, std::index_sequence<Js...>> : reverse<std::index_sequence<Is...>, std::index_sequence<I, Js...>>
{};

template <typename Seq, typename Res = std::index_sequence<>>
struct uniq;

template <typename Res>
struct uniq<std::index_sequence<>, Res>
{
    using type = typename reverse<Res>::type;    
};

template <std::size_t I, std::size_t ... Is, std::size_t ... Js>
struct uniq<std::index_sequence<I, Is...>, std::index_sequence<I, Js...>> : uniq<std::index_sequence<Is...>, std::index_sequence<I, Js...>> {};

template <std::size_t I, std::size_t ... Is, std::size_t ... Js>
struct uniq<std::index_sequence<I, Is...>, std::index_sequence<Js...>> : uniq<std::index_sequence<Is...>, std::index_sequence<I, Js...>> {};

static_assert(std::is_same_v<reverse<std::index_sequence<3, 2, 1>>::type, std::index_sequence<1, 2, 3>>); 
static_assert(std::is_same_v<uniq<std::index_sequence<1,2,2,2,3,3,3>>::type, std::index_sequence<1, 2, 3>>);

Demo

With C++20, some algorithms become even constexpr and allows:

template <std::size_t ... Is, std::size_t ... Js>
consteval auto unique_impl(std::index_sequence<Is...>, std::index_sequence<Js...>)
{
    constexpr std::array<std::size_t, sizeof...(Is)> arr = [](){
        std::array<std::size_t, sizeof...(Is)> arr{{Is...}};
        //std::sort(arr.begin(), arr.end());
        std::unique(arr.begin(), arr.end());
        return arr;
    }();

    return std::index_sequence<arr[Js]...>{};
}

template <std::size_t ... Is>
consteval auto unique_impl(std::index_sequence<Is...> seq)
{
    constexpr std::size_t size = [](){
        std::array<std::size_t, sizeof...(Is)> arr{{Is...}};
        //std::sort(arr.begin(), arr.end());
        auto it = std::unique(arr.begin(), arr.end());
        return std::distance(arr.begin(), it);
    }();
    return unique_impl(seq, std::make_index_sequence<size>());
}

template <std::size_t ... Is>
using unique = decltype(unique_impl(std::index_sequence<Is...>{}));

static_assert(std::is_same_v<unique<1,2,2,2,3,3,3>, std::index_sequence<1, 2, 3>>);

Demo

Note: constexpr std::vector would normally even allow to remove duplicated code in lambda.

Ocieock answered 23/2, 2021 at 15:59 Comment(3)
Nice. Might be good to point out, though, that this requires the original pack to be sorted; static_assert(std::is_same_v<uniq<std::index_sequence<1,3,2,2,2,3,3,3>>::type, std::index_sequence<1, 3, 2>>); fails (resolves to std::index_sequence<1, 3, 2, 3>).Incept
@dfrib: Similar to (runtime) std::unique.Ocieock
Sorting is also constexpr in C++20 :)Pelion
P
1

Using C++20, STL only

I use something similar in the gcl library.
(Please note that you can replace deduplicate_values by a IIFE lambda, if your compiler allows lambda in unevaluated context)

Live example here : https://godbolt.org/z/KzjWrM

template <auto ... values>
struct sequence{};

template <auto ... values>
constexpr auto deduplicate_values()
{
    constexpr auto arguments = std::tuple{values...};
    constexpr auto element_if_predicate = [arguments]<auto argument, std::size_t argument_index>() consteval {
        constexpr auto has_previous_position = [&arguments]<std::size_t ... indexes>(std::index_sequence<indexes...>) consteval {
            return (((std::get<indexes>(arguments) == argument) || ...));
        }(std::make_index_sequence<argument_index>{});
        if constexpr (has_previous_position)
            return std::tuple{};
        else return std::tuple{argument};
    };

    constexpr auto deduplicated_values_as_tuple = [&arguments, element_if_predicate]<std::size_t ... indexes>(std::index_sequence<indexes...>) consteval {
        return std::tuple_cat(element_if_predicate.template operator()<std::get<indexes>(arguments), indexes>()...);
    }(std::make_index_sequence<std::tuple_size_v<decltype(arguments)>>{});

    return [&deduplicated_values_as_tuple]<std::size_t ... indexes>(std::index_sequence<indexes...>) consteval {
        return sequence<std::get<indexes>(deduplicated_values_as_tuple)...>{};
    }(std::make_index_sequence<std::tuple_size_v<decltype(deduplicated_values_as_tuple)>>{});
}

template <auto ... values>
using deduplicated_sequence = decltype(deduplicate_values<values...>());

static_assert(std::is_same_v<sequence<1,2,3>, deduplicated_sequence<1,2,3,1,2,3>>);

Explanations :

  • Wrap values into a tuple
  • Create a predicate that will check if a value previously exists in values...
    • returns an empty std::tuple on failure
    • returns a std::tuple{value} on success
  • Expand values... as std::tuple_cat(predicate(...))
  • repack std::tuple elements into your desired type, sequence

gcl library code :

template <auto ... values>
constexpr auto tuple_erase_duplicate_values()
{
    constexpr auto arguments = std::tuple{values...};

    constexpr auto element_if_predicate = [arguments]<auto argument, std::size_t argument_index>() consteval {
        constexpr auto has_previous_position = [&arguments]<std::size_t ... indexes>(std::index_sequence<indexes...>) consteval {
            return (((std::get<indexes>(arguments) == argument) || ...));
        }(std::make_index_sequence<argument_index>{});

        if constexpr (has_previous_position)
            return std::tuple{};
        else return std::tuple{argument};
    };

    return [&arguments, element_if_predicate]<std::size_t ... indexes>(std::index_sequence<indexes...>) consteval {
        return std::tuple_cat(element_if_predicate.template operator()<std::get<indexes>(arguments), indexes>()...);
    }(std::make_index_sequence<std::tuple_size_v<decltype(arguments)>>{});
}
static_assert(tuple_erase_duplicate_values<'a', 'b', 'a', 1, 2, 1>() == std::tuple{'a', 'b', 1, 2});

Which is, in my own opinion, less cryptic so easier to read/maintain; while remaining compile-time.
Also, it avoids recursion.

Probe answered 2/3, 2021 at 0:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.