Why constructor can't infer template argument from concept in this situation? Also maybe there is a way to not specify concrete template type in serializer concept.
#include <span>
template <class impl_t>
concept stream = requires(impl_t &impl) {
{ impl.write(std::span<const std::byte>{}) };
};
struct any_t {};
template <class impl_t, class stream_t>
concept serializer = requires(impl_t &impl, const stream_t &stream, any_t &any) {
::stream<stream_t>;
{ impl.serialize(any) } -> std::same_as<stream_t>;
};
class memory_stream {
public:
void write(std::span<const std::byte> data) noexcept {
}
};
template <stream stream_t>
class json_serializer {
public:
template <typename T>
stream_t serialize(const T &value) {
return {};
}
};
template <stream stream_t, serializer<stream_t> serializer_t>
class client {
public:
explicit client(serializer_t &serializer) noexcept {
}
};
int main() {
json_serializer<memory_stream> s;
client c(s);
}
That's just not the way deduction works in C++20.
For class template argument deduction (CTAD), the client
class template needs to deduce two types (stream_t
and serializer_t
) and the constructor you have only uses one of those types, so the language has no idea where the other one could come from.
You can help this by providing a deduction guide for json_serializer
:
template <stream S>
client(json_serializer<S>) -> client<S, json_serializer<S>>;
And now client c(s);
works because you're telling it where the stream type comes from in this context. This could be generalized to any kind of serializer if you add some associated type for what stream a serializer is associated with.
As a guess:
template <typename S>
using stream_for = decltype(std::declval<S&>().serialize(42));
template <typename Serializer>
client(Serializer) -> client<stream_for<Serializer>, Serializer>;
Which would help if there was a unary concept for serializer
rather than a binary one.
Note that in your concept definition, the requirement:
::stream<stream_t>;
Does not check that stream_t
satisfies the stream
concept. It checks that this is a valid expression. Which it would be regardless of whether the concept is satisfied or not (false
is just as much a valid expression as true
).
This needs to be:
requires ::stream<stream_t>;
Or, even better:
template <class Serializer, class Stream>
concept serializer_for =
stream<Stream>
&& requires (Serializer& s, any_t a) {
{ s.serialize(a) } -> std::same_as<Stream>;
};