I am trying to find the most efficient and clear way to define a C++20 concept to check that all types in a parameter pack are unique.
So far I have managed to do it like this (code excerpt):
template <size_t N, typename... Ts>
// recovers N-th typename from a parameter pack
using get_Nth_type = typename std::tuple_element_t<N, std::tuple<Ts...>>;
template <size_t N, typename... Ts>
// recovers N-th value of a corresponding type from a parameter pack
constexpr get_Nth_type<N, Ts...> get_Nth_value(Ts&&... values) { return std::get<N>(std::forward_as_tuple(std::forward<Ts>(values)...)); }
template<typename... Ts>
// concept is valid when all types in a parameter pack are the same
concept all_same = (std::same_as<get_Nth_type<0, Ts...>, Ts> and ...);
static_assert(all_same<int, float, float>); // fail
static_assert(all_same<float, float, float>); // OK
template <typename T, typename... Ts>
// concept is valid when a type T is among types in a parameter pack Ts
concept any_of = (std::same_as<T, Ts> or ...);
template<size_t N, typename... Ts, int... Js>
// compile-time boolean is true when N-th type in a parameter pack is equal to any other type in the pack
constexpr bool any_same_as_Nth_helper(std::integer_sequence<int, Js...>) { return ((Js != N && std::same_as<get_Nth_type<N, Ts...>, Ts>) or ...); }
template<size_t N, typename... Ts>
// concept is valid when N-th type in a parameter pack is equal to any other type in the pack
concept any_same_as_Nth = (any_same_as_Nth_helper<N, Ts...>(std::make_integer_sequence<int, sizeof...(Ts)>()));
static_assert(any_same_as_Nth<0, int, float, float>); // fail
static_assert(any_same_as_Nth<1, int, float, float>); // OK
template<typename... Ts, int... Is>
// compile-time boolean is true is valid when any type in a parameter pack Ts contains a duplicate
constexpr bool any_same_helper(std::integer_sequence<int, Is...>) { return ((any_same_as_Nth<Is, Ts...>) or ...); }
template<typename... Ts>
// concept is valid when all types in a parameter pack are unique
concept all_unique = (not any_same_helper<Ts...>(std::make_integer_sequence<int, sizeof...(Ts)>()));
static_assert(all_unique<int, float, int>); // fail
static_assert(all_unique<float, float, int>); // fail
static_assert(all_unique<int, float, double>); // OK
This works, but I am not sure about its efficiency for large packs, since it tests every possible pair from the parameter pack (with O(N^2) complexity).
Could anybody show me how to implement the same concept with, e.g., O(N log(N)) complexity - maybe using a binary tree search?
Starting with a base that looks like this:
#include <cstddef>
template<typename T>
constexpr const void* constexpr_typeid = &constexpr_typeid<T>;
template<typename... T>
concept all_unique = []{
std::array<const void*, sizeof...(T)> tinfo = { constexpr_typeid<T>... };
for (std::size_t i = 0; i < sizeof...(T); ++i) {
for (std::size_t j = i+1u; j < sizeof...(T); ++j) {
if (tinfo[i] == tinfo[j])
return false;
}
}
return true;
}();
Which is already a lot more readable. To get an O(n lg n) solution from this, we need to be able to order the types at compile time.
Using something from How to order types at compile-time?
#include <cstddef>
#include <algorithm>
#include <array>
#if defined __GNUC__
#define CONSTEXPR_TYPE_ID_ORDERED true
template<typename T>
constexpr const char* get_string_with_type() {
return __PRETTY_FUNCTION__;
}
template<typename T>
constexpr const char* constexpr_typeid = get_string_with_type<T>();
using constexpr_typeid_type = const char*;
#else
#define CONSTEXPR_TYPE_ID_ORDERED false
template<typename T>
constexpr const void* constexpr_typeid = &constexpr_typeid<T>;
using constexpr_typeid_type = const void*;
#endif
template<typename... T>
concept all_unique = []{
std::array<constexpr_typeid_type, sizeof...(T)> tinfo = { constexpr_typeid<T>... };
#if CONSTEXPR_TYPE_ID_ORDERED
std::ranges::sort(tinfo, [](const char* l, const char* r) -> bool {
return __builtin_strcmp(l, r) < 0;
});
return std::ranges::adjacent_find(tinfo) == tinfo.end();
#else
for (std::size_t i = 0; i < sizeof...(T); ++i) {
for (std::size_t j = i+1u; j < sizeof...(T); ++j) {
if (tinfo[i] == tinfo[j])
return false;
}
}
return true;
#endif
}();
static_assert(!all_unique<int, float, int>);
static_assert(!all_unique<float, float, int>);
static_assert(all_unique<int, float, double>);
static_assert(all_unique<>);
(std::ranges::sort
did not seem to work during constant evaluation with the Microsoft STL, so that uses the O(n^2) algorithm)