Metaprogramação: falha na definição da function define uma function separada

Nesta resposta eu defino um modelo baseado na propriedade is_arithmetic do tipo:

 template enable_if_t<is_arithmetic::value, string> stringify(T t){ return to_string(t); } template enable_if_t<!is_arithmetic::value, string> stringify(T t){ return static_cast(ostringstream() << t).str(); } 

O dyp sugere que, em vez da propriedade is_arithmetic do tipo, que to_string seja definido para o tipo seja o critério de seleção do modelo. Isso é claramente desejável, mas não sei como dizer:

Se std::to_string não estiver definido, use a sobrecarga de ostringstream .

Declarar os critérios to_string é simples:

 template decltype(to_string(T{})) stringify(T t){ return to_string(t); } 

É o oposto desse critério que não consigo descobrir como construir. Isso obviamente não funciona, mas espero que transmita o que estou tentando construir:

 template enable_if_t (T t){ return static_cast(ostringstream() << t).str(); } 

    Recentemente votou nos fundamentos da biblioteca TS na reunião da comissão da semana passada:

     template using to_string_t = decltype(std::to_string(std::declval())); template using has_to_string = std::experimental::is_detected; 

    Em seguida, marque o envio e / ou SFINAE no has_to_string para o conteúdo do seu coração.

    Você pode consultar o atual rascunho de trabalho do TS sobre como o is_detected e os amigos podem ser implementados. É bastante semelhante a can_apply na resposta do @ Yakk.

    Usando o void_t Walter Brown :

     template  using void_t = void; 

    É muito fácil fazer esse tipo de traço:

     template struct has_to_string : std::false_type { }; template struct has_to_string()))> > : std::true_type { }; 

    Primeiro, acho que SFINAE normalmente deve ser escondido das interfaces. Isso torna a interface confusa. Coloque o SFINAE longe da superfície e use o despacho de tags para selecionar uma sobrecarga.

    Segundo, eu até escondo a SFINAE da class de traços. Escrevendo o código “posso fazer X” é bastante comum na minha experiência que eu não quero ter que escrever código SFINAE confuso para fazê-lo. Então, em vez disso, escrevo um traço can_apply genérico e tenho um traço que o SFINAE falha ao passar os tipos errados usando o decltype .

    Em seguida, alimentamos o traço de can_apply com can_apply para can_apply e obtemos um tipo verdadeiro / falso dependendo se o aplicativo falhar.

    Isso reduz o trabalho por traço “posso fazer X” para um valor mínimo e coloca o código SFINAE um tanto complicado e frágil longe do trabalho do dia-a-dia.

    Eu uso o void_t do C ++ void_t . Implementar você mesmo é fácil (no final desta resposta).

    Uma metafunction semelhante a can_apply está sendo proposta para padronização em C ++ 1z, mas não é tão estável quanto void_t , então não estou usando.

    Primeiro, um namespace de details para ocultar a implementação do can_apply de ser encontrado acidentalmente:

     namespace details { template 

    Podemos então escrever can_apply em termos de details::can_apply , e tem uma interface melhor (não requer que o espaço extra seja passado):

     template 

    O acima é um código genérico de metaprogramação auxiliar. Uma vez que o tenhamos colocado, podemos escrever uma class de traços can_to_string forma muito can_to_string :

     template using to_string_t = decltype( std::to_string( std::declval() ) ); template using can_to_string = can_apply< to_string_t, T >; 

    e temos um traço can_to_string que é verdadeiro se formos capazes to_string um T

    O trabalho requer a escrita de um novo traço como este é agora 2-4 linhas de código simples – basta fazer um decltype using alias, e então fazer um teste can_apply nele.

    Depois disso, usamos o despacho de tags para a implementação adequada:

     template std::string stringify(T t, std::true_type /*can to string*/){ return std::to_string(t); } template std::string stringify(T t, std::false_type /*cannot to string*/){ return static_cast(ostringstream() < < t).str(); } template std::string stringify(T t){ return stringify(t, can_to_string{}); } 

    Todo o código feio está escondido no namespace de details .

    Se você precisar de um void_t , use isto:

     templatestruct voider{using type=void;}; templateusing void_t=typename voider::type; 

    que funciona na maioria dos principais compiladores do C ++ 11.

    Note que o templateusing void_t=void; mais simples templateusing void_t=void; falha ao trabalhar em alguns compiladores antigos do C ++ 11 (havia uma ambiguidade no padrão).

    Você poderia escrever um traço auxiliar para isso usando a expressão SFINAE:

     namespace detail { //base case, to_string is invalid template  auto has_to_string_helper (...) //... to disambiguate call -> false_type; //true case, to_string valid for T template  auto has_to_string_helper (int) //int to disambiguate call -> decltype(std::to_string(std::declval()), true_type{}); } //alias to make it nice to use template  using has_to_string = decltype(detail::has_to_string_helper(0)); 

    Em seguida, use std::enable_if_t::value>

    Demonstração

    Eu acho que há dois problemas: 1) Encontre todos os algoritmos viáveis ​​para um determinado tipo. 2) Selecione o melhor.

    Podemos, por exemplo, especificar manualmente uma ordem para um conjunto de algoritmos sobrecarregados:

     namespace detail { template std::string stringify(choice<0>, T&& t) { using std::to_string; return to_string(std::forward(t)); } template std::string stringify(choice<1>, char const(&arr)[N]) { return std::string(arr, N); } template std::string stringify(choice<2>, T&& t) { std::ostringstream o; o < < std::forward(t); return std::move(o).str(); } } 

    O primeiro parâmetro de function especifica a ordem entre esses algoritmos (“primeira escolha”, “segunda escolha”, ..). Para selecionar um algoritmo, simplesmente enviamos para a melhor correspondência viável:

     template auto stringify(T&& t) -> decltype( detail::stringify(choice<0>{}, std::forward(t)) ) { return detail::stringify(choice<0>{}, std::forward(t)); } 

    Como isso é implementado? Nós roubamos um pouco do Xeo @ Flaming Dangerzone e Paul @ void_t “pode ​​implementar conceitos”? (usando implementações simplificadas):

     constexpr static std::size_t choice_max = 10; template struct choice : choice { static_assert(N < choice_max, ""); }; template<> struct choice {}; #include  template struct models : std::false_type {}; template struct models()...), void())> : std::true_type {}; #define REQUIRES(...) std::enable_if_t::value>* = nullptr 

    As classs de escolha herdam de escolhas piores: a choice<0> herda da choice<1> . Portanto, para um argumento de choice<0> de tipo choice<0> , um parâmetro de function de choice<0> de tipo choice<0> é uma correspondência melhor que choice<1> , que é uma correspondência melhor que choice<2> e assim por diante [over.ics.rank p4.4

    Observe que o desempatador mais especializado se aplica somente se nenhuma das duas funções for melhor. Devido à ordem total de choice , nunca entraremos nessa situação. Isso evita que as chamadas sejam ambíguas, mesmo que vários algoritmos sejam viáveis.

    Nós definimos nossos traços de tipo:

     #include  #include  namespace helper { using std::to_string; struct has_to_string { template auto requires_(T&& t) -> decltype( to_string(std::forward(t)) ); }; struct has_output_operator { std::ostream& ostream(); template auto requires_(T&& t) -> decltype(ostream() < < std::forward(t)); }; } 

    As macros podem ser evitadas usando uma ideia da R. Martinho Fernandes :

     template using requires = std::enable_if_t::value, int>; // exemplary application: template = 0> std::string stringify(choice<0>, T&& t) { using std::to_string; return to_string(std::forward(t)); } 

    Bem, você pode pular toda a magia de metaprogramação e usar o adaptador fit::conditional da biblioteca do Fit :

     FIT_STATIC_LAMBDA_FUNCTION(stringify) = fit::conditional( [](auto x) -> decltype(to_string(x)) { return to_string(x); }, [](auto x) -> decltype(static_cast(ostringstream() < < x).str()) { return static_cast(ostringstream() < < x).str(); } ); 

    Ou ainda mais compacto, se você não se importa com as macros:

     FIT_STATIC_LAMBDA_FUNCTION(stringify) = fit::conditional( [](auto x) FIT_RETURNS(to_string(x)), [](auto x) FIT_RETURNS(static_cast(ostringstream() < < x).str()) ); 

    Note, eu também constrangei a segunda function também, então se o tipo não puder ser chamado com to_string nem transmitido para ostringstream então a function não pode ser chamada. Isso ajuda com melhores mensagens de erro e melhor composição com os requisitos de tipo de verificação.