Estou encontrando uma diferença inesperada no comportamento entre o SFINAE tradicional (usando type_traits
and std::void_t
) e os conceitos modernos do C++20 ao definir um fallback genérico operator<<
. O propósito é direto: criar um genérico operator<<
que seja habilitado somente se nenhuma definição personalizada existente operator<<
for encontrada via Argument-Dependent Lookup (ADL) .
A detecção baseada em SFINAE da velha escola usando características ( is_std_streamable
) funciona como esperado, definido como:
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 {};
E a detecção baseada em conceitosStdStreamable
( ) é definida como:
template <class T>
concept StdStreamable = requires(const T t, std::ostream& os) {
{ os << t } -> std::same_as<std::ostream&>;
};
O fallback genérico operator<<
se parece com isto ( requires
cláusulas comentadas):
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) {
...
}
Ao descomentar a cláusula baseada em conceitos ( ou ), tanto o GCC quanto o Clang produzem o seguinte erro de restrição cíclica:requires
requires(!StdStreamable<T>)
requires(!is_std_streamable<T>::value)
error: satisfaction of constraint 'StdStreamable<T>' depends on itself
Entendo que usar a std::declval<std::ostream&>() << std::declval<const T&>()
expressão em uma requires
cláusula ao definir uma nova versão de operator<<
pode ser interpretado pelo compilador como uma dependência cíclica. Mas por que os conceitos do C++20 acionam esse problema de restrição cíclica, enquanto o SFINAE tradicional não? Esse comportamento é obrigatório pelo padrão, uma limitação conhecida de conceitos ou potencialmente um bug do compilador?
Exemplo mínimo reproduzível completo e detalhes adicionais:
Desde já, obrigado.
Esta é uma violação de ODR, seu programa está malformado, você está tentando fazer
isso é proibido e a norma diz
StdStreamable<T>
ouis_std_streamable<T>
daria significado diferente dependendo de onde fosse instanciado no programa.A SFINAE não foi obrigada a diagnosticar esse bug, porque falha de substituição não é um erro e, além disso, não requer diagnóstico , então eles simplesmente aceitaram esse bug.
Os compiladores foram capazes de implementar conceitos de uma forma que detecta esse tipo de bug.
você pode usar uma função livre que escolhe entre
std::format
ouobject.print
ouostream.operator<<
dependendo de qual delas está definida, mas não pode definir uma delas se ela não existir usando metaprogramação de modelo.O problema com sobrecargas ambíguas vem das definições de funções-membro.
std::ostream::operator<<()
Seu conceito deve ser verificado apenas em relação às definições de funções-membro.Sua função de modelo autônoma genérica `operator<<()` precisa verificar esse conceito e não deve ter nenhuma especialização em relação às funções autônomas de modelo não genéricas do STL.
Se a sobrecarga do operador usar um tipo especializado para o fluxo, será sempre uma correspondência melhor.
Com isso em mente você deve escrever:
A função autônoma genérica não precisa ser restringida em relação às outras funções autônomas, pois elas já foram selecionadas ao selecionar a "melhor correspondência".
Exemplo completo: https://godbolt.org/z/KKe46fsd1
Mas é uma solução um pouco acadêmica, pois não faz muito sentido para um programa do mundo real, já que "todos" os outros tipos no seu programa devem fornecer
print()
. Neste caso, é muito mais fácil verificar o tipo para ter umaprint()
função. Não há função "genérica", pois você deve conhecer o interior de todos os tipos personalizados imprimíveis.