I'm writing a policy class, TrivialSerializer
, that'll slot into a function later in C++. This policy needs to serialize objects whose types may be specified to certain extent by other policies, and write them to a stream. Only the object value needs to be serialized, because the layout and structure of the objects are specified beforehand.
To achieve this, I'm using a generic templated static member function template <typename T> static void write(output_stream_t& stream, T const& object)
that serializes arbitrary objects, with only certain specializations and overloads of the template being non-deleted. For example, I have a generic specialization for sized ranges of the form
template <ranges::sized_range T>
requires requires(output_stream_t& stream, ranges::range_value_t<T> value) {
{ write<ranges::range_value_t<T>>(stream, value) };
}
static void write(output_stream_t& stream, T const& range);
and I have similar specializations for tuples and integrals, and overloads for specific classes that should be handled separately.
The problem here is that the way the constraints are parsed by the language doesn't correspond to the intention I had when writing them. A type like vector<vector<int>>
doesn't try to use the specialization for sized ranges, because its value type is vector<int>
, whose specialization hasn't been instantiated yet by the time the requires clause gets evaluated. The type vector<int>
itself, of course, does use the correct overload if it's used anywhere other than in that specific requires clause. The intended behavior when calling write(output_stream_t&, vector<vector<int>> const&)
is for the type checker to consider the specialization for sized ranges and look for a way to call write(output_stream_t&, vector<int> const&)
, which causes it to consider the specialization for sized ranges again and look for a way to call write(output_stream_t&, int const&)
, which it finds in the form of the integral specialization, which causes it to conclude that vector<vector<int>>
can be used as the second argument to write
.
My main question is: is there a way to cause the C++ constraint checker to perform this "recursive" consideration of overloads, or do I have to resort to runtime constraint checks within the function bodies of write
? My secondary question is: are there any proposals in the works by the committee and working groups to address this issue?
I've already seen this question, which isn't far from running into the same problem I have, but it doesn't offer any solutions in this case.
As @HolyBlackCat pointed out, using a class with specializations and a static member function (and using a separate function as a nicer interface to the specializations) solves the problem. The class specialization for ranges now looks like this:
template <sink_c sink_t, std::ranges::sized_range value_t>
requires requires(sink_t& sink, std::ranges::range_value_t<value_t> element) {
{ DefaultWrite<sink_t, std::ranges::range_value_t<value_t>>::call(sink, element) };
}
struct DefaultWrite<sink_t, value_t> {
static void call(sink_t& sink, value_t const& value);
};