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 ><