C++20 Concepts to differentiate type functionality

Viewed 64

Solution

My specific issue was that my constraints requires (T c) { c.[insert|push_back]({}); } contained {}, which unecessarily requires that default construction of the type being stored is possible.

This can be solved by expanding the constraint signatures to requires (T c, T::value_type v) { c.[insert|push_back](v); }

template <typename T>
requires std::ranges::range<T> && requires (T c, T::value_type v) { c.insert(v); }
static T Deserialise(const json& serialised)
{
    T items;
    for (const auto& item : serialised) {
        ...
    }
    return items;
}

template <typename T>
requires std::ranges::range<T> && requires (T c, T::value_type v) { c.push_back(v); }
static T Deserialise(const json& serialised)
{
    T items;
    for (const auto& item : serialised) {
        ...
    }
    return items;
}

@JHBonarius Also pointed out that I should replace my usage of the constexpr boolean std::is_same_v<T, std::string> with the concept std::same_as<T, std::string>


Question

My goal is to have a set of Deserialise functions that can handle converting any json array into the specified container type T so long as T::value_type also has its own Deserialise function.

I have two functions that do almost the same thing, except one works on containers that use insert (like std::map or std::set), and the other works on containers that use push_back (like std::vector and std::deque).

template <typename T>
requires std::ranges::range<T> && (!std::is_same_v<T, std::string>) && requires (T c) { c.insert({}); }
static T Deserialise(const json& serialised)
{
    T items;
    for (const auto& item : serialised) {
        items.insert(std::end(items), Deserialise<typename T::value_type>(item));
    }
    return items;
}

template <typename T>
requires std::ranges::range<T> && (!std::is_same_v<T, std::string>) && requires (T c) { c.push_back({}); }
static T Deserialise(const json& serialised)
{
    T items;
    for (const auto& item : serialised) {
        items.push_back(Deserialise<typename T::value_type>(item));
    }
    return items;
}

This works well except the concept requires (T c) { c.push_back({}); } doesn't just make sure type T has a push_back function, it also inadvertantly makes it a requirement that the T::value_type must be trivially constructable, which is undesirable. I have tried detecting push_back more directly:

template<typename T>
using PushBackable_t = decltype( &T::push_back );
template<typename T>
constexpr bool IsPushBackable = std::experimental::is_detected_v<PushBackable_t, T>;

But unfortunately this fails the following assert...

static_assert(IsPushBackable<std::vector<int>>);

I have also tried requires (T c) { std::back_inserter(c); } but this leaves the templates ambiguous.

I was previously using the constraints

template <typename Test, template<typename...> class Ref>
constexpr inline bool IsInstance = false;
template <template<typename...> class Ref, typename... Args>
constexpr inline bool IsInstance<Ref<Args...>, Ref> = true;
template<typename T>
constexpr inline bool IsVector = IsInstance<T, std::vector>;

template<typename T>
constexpr inline bool IsMap = IsInstance<T, std::map>;

But this is not generic enough, and I don't want to manually add a new constraint each time a new type needs supporting.

Looking abck on it I'm also not entirely sure how requires (T c) { c.insert({}); } is working when insert takes two arguments...

I realise I'm using a lot of constexpr inline bools instead of the concept keyword, I've just begun investigating c++20 features and am in the process of moving the code over, but until I'm sure of what I'm doing, if it ain't broke, don't fix it and all that ><

0 Answers
Related