In my question I received an answer which suits me, but I don't understand how does it work.
Especially, I don't understand how delete keyword along with concepts remove overloads for operator<<.
(I will paste piece-by-piece, refactored version of the code from the accepted answer.)
enum class LogLevel
{
info,
warning,
error
};
template<typename T>
concept HasLogMethodReturningReferenceToSelf = requires(T v)
{
{
v.log(LogLevel{})
} -> std::convertible_to<T&>;
};
So, here we define a concept, which checks whether a type has method log(), which takes LogLevel as a parameter and returns convertible to reference to self.
Then we delete operator<< overloads (block implicit function generation and explicit overloads) for overloads which have on the left-side of << a type which satisfies HasLogMethodReturningReferenceToSelf and on the right-side of << a type which is not a std::string:
template<HasLogMethodReturningReferenceToSelf T, class U>
requires(!std::convertible_to<U, std::string>) auto operator<<(T, U) = delete;
- I don't understand when the overloads would be deleted? During "instantiation" of the concept for some specific type
T? Because, not for each type which satisfies the criteria (it would break the codebase?)?
Because later, another concept is defined, which checks for output stream operator overloads for basic types:
template<typename T>
concept HasOutputStreamOperatorOverloadsForBasicTypes =
requires(T v, unsigned unsigned_, int int_, float float_, unsigned char unsigned_char_, char char_)
{
{
v << "string literal" //
<< unsigned_ //
<< int_ //
<< float_ //
<< unsigned_char_ //
<< char_ //
} -> std::convertible_to<T&>;
};
Finally, we define a Loggable concept:
template<typename T>
concept Loggable = HasLogMethodReturningReferenceToSelf<T> && HasOutputStreamOperatorOverloadsForBasicTypes<T>;
Normally, I would define one big Loggable concept:
template<typename T>
concept Loggable = requires(T v)
{
{
v.log(LogLevel{})
} -> std::convertible_to<T&>;
{
v << "string literal" //
<< unsigned_ //
<< int_ //
<< float_ //
<< unsigned_char_ //
<< char_ //
} -> std::convertible_to<T&>;
};
And then somehow restrict implicit conversion. I guess that the author of the answer had to split those requires expressions to two, because otherwise, when I use the latter Loggable definition and then delete the overloads after the definition:
template<Loggable T, class U>
requires(!std::convertible_to<U, std::string>) auto operator<<(T, U) = delete;
I get compile error:
fatal error: recursive template instantiation exceeded maximum depth of 1024
- Why?
When we check whether
v << unsigned_is well-formed in the requires clause, if yourLoggerclass does not have theunsignedoverload ofoperator<<, the globaloperator<<defined before concepts will be selected sinceUwill be directly instantiated asunsignedand there is no implicit conversion, so it has a higher priority.And because the global
operator<<is deleted, sov << unsigned_is ill-formed and the constraint is not satisfied.