Fechar contatos ADL


Como escrever seu nome na história para sempre? O primeiro a voar para a lua? O primeiro a encontrar uma mente alienígena? Temos uma maneira mais simples - você pode se encaixar no padrão da linguagem C ++.


Eric Nibler, autor de C ++ Ranges, fornece um bom exemplo. Lembre-se disso. 19 de fevereiro de 2019 é o dia em que o termo “niblóide” foi falado pela primeira vez na reunião do WG21 ”, ele escreveu no Twitter.


De fato, se você for para CppReference, na seção cpp / algorítimo / rangescpp / algoritmo / intervalos , encontrará muitas referências lá (niebloid). Para isso, um modelo wiki dsc_niebloid separado foi criado.


Infelizmente, não encontrei nenhum artigo oficial sobre esse assunto e decidi escrever o meu. Esta é uma jornada pequena, mas fascinante, para os abismos da astronáutica arquitetônica, na qual podemos mergulhar no abismo da loucura das AVDs e nos familiarizar com os niblóides.


Importante: Eu não sou um soldador de verdade, mas um javist que às vezes corrige erros no código C ++ conforme necessário. Se você demorar um pouco para ajudar a encontrar erros no raciocínio, isso seria bom. "Ajude o viajante Dasha a coletar algo razoável."


Lookup


Primeiro você precisa decidir sobre os termos. Essas são coisas bem conhecidas, mas “o explícito é melhor que o implícito”, portanto, discutiremos separadamente. Não uso terminologia real no idioma russo, mas uso o inglês. Isso é necessário porque mesmo a palavra "restrição" no contexto deste artigo pode ser associada a pelo menos três versões em inglês, cuja diferença é importante para a compreensão.


Por exemplo, em C ++, existe o conceito de uma pesquisa por nome ou, em outras palavras, uma pesquisa: quando um nome é encontrado em um programa, ele é compilado com sua declaração durante a compilação.


Uma pesquisa pode ser qualificada (se o nome estiver à direita do operador de permissão do escopo :: :) e não qualificado em outros casos. Se a pesquisa for qualificada, ignoramos os membros correspondentes da classe, namespace ou enumeração. Pode-se chamar isso de versão “completa” do registro (como parece ser feito na tradução de Straustrup), mas é melhor deixar a ortografia original, porque isso se refere a um tipo muito específico de completude.


ADL


Se a pesquisa não for qualificada, precisamos entender exatamente onde procurar o nome. E aqui está um recurso especial chamado ADL: pesquisa dependente de argumento , ou então - a busca por Koenig (aquele que cunhou o termo "antipadrão", que é um pouco simbólico à luz do texto a seguir). Nicolai Josuttis em seu livro “A biblioteca padrão do C ++: tutorial e referência” descreve o seguinte: “O ponto é que você não precisa qualificar o espaço para nome da função se pelo menos um dos tipos de argumento estiver definido no espaço para nome dessa função”.


Como deve ser?


 #include <iostream> int main() { //  . //   , operator<<    ,  ADL , //    std    std::operator<<(std::ostream&, const char*) std::cout << "Test\n"; //    .      -     . operator<<(std::cout, "Test\n"); // same, using function call notation //    : // Error: 'endl' is not declared in this namespace. //      endl(),  ADL  . std::cout << endl; //  . //    ,       ADL. //     std,   endl      std. endl(std::cout); //    : // Error: 'endl' is not declared in this namespace. //  ,  - (endl) -     . (endl)(std::cout); } 

Vá para o inferno com a ADL


Parece simples. Ou não? Primeiro, dependendo do tipo de argumento, a ADL trabalha de nove maneiras diferentes , para matar com uma vassoura.


Em segundo lugar, puramente prático, imagine que temos algum tipo de função de troca. Acontece que std::swap(obj1,obj2); e using std::swap; swap(obj1, obj2); using std::swap; swap(obj1, obj2); pode se comportar de maneira completamente diferente. Se a ADL estiver ativada, em vários swaps diferentes, o que você precisa já estará selecionado com base nos namespaces dos argumentos! Dependendo do ponto de vista, esse idioma pode ser considerado um exemplo positivo e um negativo :-)


Se lhe parecer que isso não é suficiente, você pode soltar a lenha no forno do chapéu. Isso foi recentemente bem escrito por Arthur O'Dwyer . Espero que ele não me castigue por usar seu exemplo.


Imagine que você tem um programa desse tipo:


 #include <stdio.h> namespace A { struct A {}; void call(void (*f)()) { f(); } } void f() { puts("Hello world"); } int main() { call(f); } 

Obviamente, ele não compila com um erro:


 error: use of undeclared identifier 'call'; did you mean 'A::call'? call(f); ^~~~ A::call 

Mas se você adicionar uma sobrecarga completamente não utilizada da função f , tudo funcionará!


 #include <stdio.h> namespace A { struct A {}; void call(void (*f)()) { f(); } } void f() { puts("Hello world"); } void f(A::A); // UNUSED int main() { call(f); } 

No Visual Studio, ele ainda será interrompido, mas esse é o destino dela, não funcionando.


Como isso aconteceu? Vamos nos aprofundar no padrão (sem tradução, porque essa tradução seria uma mistura extraordinariamente monstruosa de chavões):


Se o argumento for o nome ou o endereço de um conjunto de funções sobrecarregadas e / ou modelos de função, suas entidades e espaços para nome associados serão a união daqueles associados a cada um dos membros do conjunto, ou seja, as entidades e espaços para nome associados ao seu parâmetro tipos e tipo de retorno. [...] Além disso, se o conjunto de funções sobrecarregadas mencionado acima for nomeado com um ID de modelo, suas entidades e espaços de nome associados também incluirão os de seus argumentos de modelo e de modelo.

Agora pegue um código como este:


 #include <stdio.h> namespace B { struct B {}; void call(void (*f)()) { f(); } } template<class T> void f() { puts("Hello world"); } int main() { call(f<B::B>); } 

Nos dois casos, são obtidos argumentos que não têm tipo. f e f<B::B> são os nomes dos conjuntos de funções sobrecarregadas (da definição acima) e esse conjunto não tem tipo. Para recolher uma sobrecarga em uma única função, é necessário entender que tipo de ponteiro de função é mais adequado para a melhor sobrecarga de call . Portanto, você precisa coletar um conjunto de candidatos à call , o que significa iniciar uma consulta à call nome. E para este ADL começará!


Mas geralmente para ADL, devemos conhecer os tipos de argumentos! E aqui Clang, ICC e MSVC quebram erroneamente da seguinte maneira (mas o GCC não):


 [build] ..\..\main.cpp(15,5): error: use of undeclared identifier 'call'; did you mean 'B::call'? [build] call(f<B::B>); [build] ^~~~ [build] B::call [build] ..\..\main.cpp(4,10): note: 'B::call' declared here [build] void call(void (*f)()) { [build] ^ 

Até os criadores de compiladores com ADL têm um relacionamento um pouco tenso.


Bem, a ADL ainda parece uma boa ideia? Por um lado, não precisamos mais escrever um código tão servil de maneira educada:


 std::cout << "Hello, World!" << std::endl; std::operator<<(std::operator<<(std::cout, "Hello, World!"), "\n"); 

Por outro lado, trocamos por brevidade o fato de que agora existe um sistema que funciona de maneira completamente desumana. Uma história trágica e majestosa sobre como a facilidade de escrever o Halloworld pode afetar todo o idioma em uma escala de décadas.


Intervalos e conceitos


Se você abrir a descrição da biblioteca Nibler Rangers , mesmo antes da menção de niblóides, você encontrará muitos outros marcadores chamados (conceito) . Isso já é uma coisa bonita, mas por precaução (para idosos e javistas), lembrarei o que é .


Os conceitos são chamados de conjuntos nomeados de restrições que se aplicam aos argumentos do modelo para selecionar as melhores sobrecargas de função e as especializações de modelo mais adequadas.


 template <typename T> concept bool HasStringFunc = requires(T a) { { to_string(a) } -> string; }; void print(HasStringFunc a) { cout << to_string(a) << endl; } 

Aqui nós impusemos uma restrição de que o argumento deve ter uma função to_string que retorne uma string. Se tentarmos colocar algum jogo na print que não se enquadre nas restrições, esse código simplesmente não será compilado.


Isso simplifica bastante o código. Por exemplo, veja como Nibler classificou nos intervalos-v3 , que funciona em C ++ 14/11/17. Existe um código maravilhoso como este:


 #define CONCEPT_PP_CAT_(X, Y) X ## Y #define CONCEPT_PP_CAT(X, Y) CONCEPT_PP_CAT_(X, Y) /// \addtogroup group-concepts /// @{ #define CONCEPT_REQUIRES_(...) \ int CONCEPT_PP_CAT(_concept_requires_, __LINE__) = 42, \ typename std::enable_if< \ (CONCEPT_PP_CAT(_concept_requires_, __LINE__) == 43) || (__VA_ARGS__), \ int \ >::type = 0 \ /**/ 

Para que mais tarde você possa fazer:


 struct Sortable_ { template<typename Rng, typename C = ordered_less, typename P = ident, typename I = iterator_t<Rng>> auto requires_() -> decltype( concepts::valid_expr( concepts::model_of<concepts::ForwardRange, Rng>(), concepts::is_true(ranges::Sortable<I, C, P>()) )); }; using Sortable = concepts::models<Sortable_, Rng, C, P>; template<typename Rng, typename C = ordered_less, typename P = ident, CONCEPT_REQUIRES_(!Sortable<Rng, C, P>())> void operator()(Rng &&, C && = C{}, P && = P{}) const { ... 

Espero que você já queira ver tudo isso e apenas usar conceitos preparados em um compilador novo.


Pontos de personalização


A próxima coisa interessante que pode ser encontrada no padrão é customization.point.object . Eles são usados ​​ativamente na biblioteca Nibler Ranges.


O ponto de personalização é uma função usada pela biblioteca padrão para que possa ser sobrecarregada para tipos de usuário no espaço de nomes do usuário, e essas sobrecargas podem ser encontradas usando ADL.


Os pontos de personalização são projetados com os seguintes princípios de arquitetura em cust ( cust é o nome de algum ponto de personalização imaginário):


  • O código que chama cust escrito no formato qualificado std::cust(a) ou no não qualificado: using std::cust; cust(a); using std::cust; cust(a); . Ambas as entradas devem se comportar de forma idêntica. Em particular, eles devem encontrar qualquer sobrecarga de usuário no espaço para nome associado aos argumentos.
  • Código que usa cust na forma de uma std::cust; cust(a); std::cust; cust(a); não deve ser capaz de contornar as restrições impostas ao std::cust .
  • As chamadas pontuais personalizadas devem funcionar de maneira eficiente e otimizada em qualquer compilador bastante moderno.
  • A decisão não deve criar novas violações da regra de definição única (ODR) .

Para entender o que é, você pode dar uma olhada no N4381 . À primeira vista, eles parecem uma maneira de escrever suas próprias versões de begin , swap , data e similares, e a biblioteca padrão as escolhe usando ADL.


A questão é: como isso difere da prática antiga, quando o usuário escreve uma sobrecarga para alguns begin para seu próprio tipo e espaço para nome? E por que eles são objetos?


De fato, são instâncias de objetos funcionais no std . Seu objetivo é primeiro obter verificações de tipo (projetadas como conceitos) em todos os argumentos em uma linha e, em seguida, despachar a chamada para a função correta no std ou entregá-la à venda em ADL.


De fato, esse não é o tipo de coisa que você usaria em um programa regular que não é da biblioteca. Esse é um recurso da biblioteca padrão, que permitirá adicionar verificações de conceitos em futuros pontos de extensão, o que, por sua vez, levará à exibição de erros mais bonitos e compreensíveis se você estragar alguma coisa nos modelos.


A abordagem atual dos pontos de personalização tem alguns problemas. Em primeiro lugar, é muito fácil quebrar tudo. Imagine este código:


 template<class T> void f(T& t1, T& t2) { using std::swap; swap(t1, t2); } 

Se acidentalmente fizermos uma chamada qualificada para std::swap(t1, t2) nossa própria versão do swap nunca será iniciada, independentemente do que colocarmos lá. Mais importante, porém, não há como anexar centralmente verificações de conceito a essas implementações de funções personalizadas. No N4381, eles escrevem:


“Imagine que algum dia no futuro, std::begin exija que seu argumento seja modelado como um conceito de Range . Adicionar essa restrição simplesmente não afetará o código idiomamente usando std::begin :


 using std::begin; begin(a); 

Afinal, se a chamada begin for despachada para a versão sobrecarregada criada pelo usuário, as restrições em std::begin simplesmente ignoradas. ”


A solução descrita no propozal resolve os dois problemas; para isso, usamos a abordagem desta implementação especulativa do std::begin (você pode ver o godbolt ):


 #include <utility> namespace my_std { namespace detail { struct begin_fn { /*   ,         begin(arg)  arg.begin().  -   . */ template <class T> auto operator()(T&& arg) const { return impl(arg, 1L); } template <class T> auto impl(T&& arg, int) const requires requires { begin(std::declval<T>()); } { return begin(arg); } // ADL template <class T> auto impl(T&& arg, long) const requires requires { std::declval<T>().begin(); } { return arg.begin(); } // ... }; } //        inline constexpr detail::begin_fn begin{}; } 

Uma chamada qualificada de alguns my_std::begin(someObject) sempre passa por my_std::detail::begin_fn - e isso é bom. O que acontece com uma chamada não qualificada? Vamos ler nosso artigo novamente:


“No caso em que begin é chamado sem qualificação imediatamente após o aparecimento do my_std::begin dentro do escopo, a situação muda um pouco. No primeiro estágio da pesquisa, o nome begin resolvido para o objeto global my_std::begin . Como a pesquisa encontrou um objeto, não uma função, a segunda fase da pesquisa não é executada. Em outras palavras, se my_std::begin for um objeto, use a construção my_std::detail::begin_fn begin; begin(a); my_std::detail::begin_fn begin; begin(a); simplesmente equivalente a std::begin(a); "E, como vimos, isso lança ADL personalizado".


É por isso que a validação de conceito pode ser feita em um objeto de função no std antes que o ADL chame a função fornecida pelo usuário. Não há como enganar esse comportamento.


Como os pontos de personalização são personalizados?


De fato, “objeto de ponto de personalização” (CPO) não é um bom nome. Pelo nome, não está claro como eles se expandem, quais mecanismos estão ocultos, quais funções eles preferem ...


O que nos leva ao termo "niblóide". Um niblóide é um CPO que chama a função X se for definido na classe; caso contrário, chama a função X se houver uma função livre adequada; caso contrário, tenta executar algum fallback da função X.


Portanto, por exemplo, o nibloid ranges::swap ao chamar o ranges::swap(a, b) primeiro tentará chamar a.swap(b) . Se não houver esse método, ele tentará chamar swap(a, b) usando ADL. Se isso não funcionar, tente auto tmp = std::move(a); a = std::move(b); b = std::move(tmp) auto tmp = std::move(a); a = std::move(b); b = std::move(tmp) auto tmp = std::move(a); a = std::move(b); b = std::move(tmp) .


Sumário


Como Matt brincou no Twitter, Dave sugeriu uma vez fazer objetos funcionais "funcionarem" com o ADL, assim como as funções regulares, por razões de consistência. A ironia é que a capacidade deles de desabilitar o ADL e ficar invisível para ele agora se tornou suas principais vantagens.


Este artigo inteiro foi uma preparação para isso.


" Acabei de entender tudo, só isso. Você vai ouvir ?


Você já olhou alguma coisa, e parecia loucura, e então sob uma luz diferente
coisas loucas vê-los normais?



Não tenha medo. Não tenha medo. Eu me sinto tão bem no coração. Tudo vai ficar bem. Não me sinto tão bem há muitos anos. Tudo vai ficar bem.



Minuto de publicidade. Já nesta semana , de 19 a 20 de abril, o C ++ Russia 2019 será realizado - uma conferência cheia de apresentações hardcore, tanto no próprio idioma quanto em questões práticas como multithreading e performance. A propósito, a conferência é aberta por Nicolai Josuttis, autor da Biblioteca Padrão C ++: um tutorial e uma referência , mencionados no artigo. Você pode se familiarizar com o programa e comprar ingressos no site oficial . Há muito pouco tempo, esta é a última chance.

Source: https://habr.com/ru/post/pt447900/


All Articles