c++ - Why do C++20 concepts cause cyclic constraint errors while old-school SFINAE does not? - Stack Overflow

admin2025-04-10  0

I'm encountering an unexpected difference in behavior between traditional SFINAE (using type_traits and std::void_t) and modern C++20 concepts when defining a generic fallback operator<<. The purpose is straightforward: to create a generic operator<< that is enabled only if no existing custom-defined operator<< is found via Argument-Dependent Lookup (ADL).

The old-school SFINAE-based detection using traits (is_std_streamable) works as expected, defined as:

template <class T, class = void>
struct is_std_streamable : std::false_type {};

template <class T>
struct is_std_streamable<T, std::void_t<decltype(std::declval<std::ostream&>() << std::declval<const T&>())>> : std::true_type {};

And the concepts-based detection (StdStreamable) is defined as:

template <class T>
concept StdStreamable = requires(const T t, std::ostream& os) {
    { os << t } -> std::same_as<std::ostream&>;
};

The generic fallback operator<< looks like this (requires clauses commented out):

template <StdPrintable T>
// requires(!StdStreamable<T>)
// requires(!is_std_streamable<T>::value)
std::enable_if_t<!is_std_streamable<T>::value, std::ostream&>
operator<<(std::ostream& os, T const& val) {
...
}

When uncommenting the concepts-based requires clause (either requires(!StdStreamable<T>) or requires(!is_std_streamable<T>::value)), both GCC and Clang produce the following cyclic constraint error:

error: satisfaction of constraint 'StdStreamable<T>' depends on itself

I understand, that using the std::declval<std::ostream&>() << std::declval<const T&>() expression in a requires clause when defining a new version of operator<< can be interpreted by the compiler as a cyclic dependency. But why do C++20 concepts trigger this cyclic constraint issue, whereas traditional SFINAE does not? Is this behavior mandated by the standard, a known limitation of concepts, or potentially a compiler bug?

Full minimal reproducible example and additional details:

Thanks in advance.

I'm encountering an unexpected difference in behavior between traditional SFINAE (using type_traits and std::void_t) and modern C++20 concepts when defining a generic fallback operator<<. The purpose is straightforward: to create a generic operator<< that is enabled only if no existing custom-defined operator<< is found via Argument-Dependent Lookup (ADL).

The old-school SFINAE-based detection using traits (is_std_streamable) works as expected, defined as:

template <class T, class = void>
struct is_std_streamable : std::false_type {};

template <class T>
struct is_std_streamable<T, std::void_t<decltype(std::declval<std::ostream&>() << std::declval<const T&>())>> : std::true_type {};

And the concepts-based detection (StdStreamable) is defined as:

template <class T>
concept StdStreamable = requires(const T t, std::ostream& os) {
    { os << t } -> std::same_as<std::ostream&>;
};

The generic fallback operator<< looks like this (requires clauses commented out):

template <StdPrintable T>
// requires(!StdStreamable<T>)
// requires(!is_std_streamable<T>::value)
std::enable_if_t<!is_std_streamable<T>::value, std::ostream&>
operator<<(std::ostream& os, T const& val) {
...
}

When uncommenting the concepts-based requires clause (either requires(!StdStreamable<T>) or requires(!is_std_streamable<T>::value)), both GCC and Clang produce the following cyclic constraint error:

error: satisfaction of constraint 'StdStreamable<T>' depends on itself

I understand, that using the std::declval<std::ostream&>() << std::declval<const T&>() expression in a requires clause when defining a new version of operator<< can be interpreted by the compiler as a cyclic dependency. But why do C++20 concepts trigger this cyclic constraint issue, whereas traditional SFINAE does not? Is this behavior mandated by the standard, a known limitation of concepts, or potentially a compiler bug?

Full minimal reproducible example and additional details:

  • https://godbolt./z/be7Yqxo93

Thanks in advance.

Share Improve this question edited Mar 23 at 16:44 康桓瑋 43.4k5 gold badges63 silver badges126 bronze badges asked Mar 23 at 13:39 Petr FilipskýPetr Filipský 1936 bronze badges 2
  • 2 Note this using concept and template specialization works as well. – Weijun Zhou Commented Mar 23 at 14:55
  • May be something related: stackoverflow/questions/76624030/… – Fedor Commented Mar 24 at 17:12
Add a comment  | 

2 Answers 2

Reset to default 20

This is an ODR violation, your program is ill-formed, you are trying to do

if (not_defined(X)) define(X)

this is forbidden and the standard says

If two different points of instantiation give a template specialization different meanings according to the one-definition rule, the program is ill-formed, no diagnostic required.

StdStreamable<T> or is_std_streamable<T> would give different meaning depending on where it is instantiated in the program.

SFINAE wasn't required to diagnose this bug, because substitution failure is not an error, and the fact that it is a no diagnostics required bug, so they just accepted this bug.

Compilers were able to implement concepts in a way that detects this form of bug.


you can use a free function that picks from std::format or object.print or ostream.operator<< depending on which of them is defined, but you cannot define one of them if it doesn't exist using template metaprogramming.

The problem with ambiguous overloads are coming from the member function definitions from std::ostream::operator<<() Your concept should check only against the member function definitions.

Your generic free standing template function `operator<<()` has than to check against that concept and must not have any specialization against the non generic template free standing functions from the STL.

If your operator overload uses a specialized type for the stream, it is always a better match.

With this in mind you have to write:

    template < typename T>
concept HasShiftOut = requires ( T t )
{
    std::declval<std::ostream>().operator<<(t);
};

template<typename _CharT, typename _Traits, typename T>
inline std::basic_ostream<_CharT, _Traits>&
operator<<(std::basic_ostream<_CharT, _Traits>& os, const T& t )
    requires (!HasShiftOut<T>)
{
    os << "generic: ";
    t.print(os);
    os << std::endl;
    return os;
}
template<typename _CharT, typename _Traits>
inline std::basic_ostream<_CharT, _Traits>&
operator<<(std::basic_ostream<_CharT, _Traits>& os, const A& a )
{
    return os << "custom A: " << a.a << std::endl;
}
std::ostream& operator<<(std::ostream& s, const B& b) { return s << "custom B: " << b.b << std::endl; }

The generic free standing function needs not to be restricted against the other free standing functions, as they already selected by selecting the "better match".

Full example: https://godbolt./z/KKe46fsd1

But it is a bit a academic solution, as it makes not really sense for a real world program, as "all" other types in your program must provide print(). In this case, it is much easier to check the type to have a print() function. There is no "generic" function, as you must know the insides of all printable custom types.

转载请注明原文地址:http://conceptsofalgorithm.com/Algorithm/1744284786a239659.html

最新回复(0)