Search code examples
c++metaprogrammingc++20c++-concepts

Constructor can't infer template argument from concept


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);
}

Godbolt link


Solution

  • 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>;
           };