C ++: uma sessão de arqueologia espontânea e por que você não deve usar funções variáveis ​​no estilo de C

Tudo começou, como sempre, com um erro. Esta é a primeira vez que trabalhei com a Java Native Interface e, na parte C ++, envolvi uma função que cria um objeto Java. Essa função - CallVoidMethod - é variável, ou seja, além de um ponteiro para o ambiente JNI , um ponteiro para o tipo de objeto a ser criado e um identificador para o método chamado (neste caso, o construtor), é necessário um número arbitrário de outros argumentos. O que é lógico, porque esses outros argumentos são transmitidos para o método chamado no lado Java e os métodos podem ser diferentes, com um número diferente de argumentos de qualquer tipo.

Consequentemente, também fiz minha variável wrapper. Para passar um número arbitrário de argumentos para o CallVoidMethod usei va_list , porque é diferente nesse caso. Sim, foi isso que o va_list enviou para o CallVoidMethod . E eliminou a falha de segmentação banal da JVM.

Em duas horas, tentei várias versões da JVM, de 8 a 11, porque: em primeiro lugar, esta é minha primeira experiência com a JVM e, nesse caso, confiei no StackOverflow mais do que eu e, em segundo lugar, em alguém nesse caso, no StackOverflow, eu recomendei, neste caso, não usar o OpenJDK, mas o OracleJDK, e não 8, mas 10. E só então finalmente notei que, além da variável CallVoidMethodCallVoidMethodV , que recebe um número arbitrário de argumentos via va_list .

O que eu mais não gostei nessa história foi que não percebi imediatamente a diferença entre as reticências ( va_list ) e va_list . E, tendo notado, não consegui explicar para mim qual era a diferença fundamental. Portanto, precisamos lidar com reticências, e com va_list e (já que ainda estamos falando sobre C ++) com modelos de variáveis.

O que dizer das reticências e va_list é dito no Padrão


O padrão C ++ descreve apenas as diferenças entre seus requisitos e os do padrão C. As diferenças em si serão discutidas mais adiante, mas por enquanto explicarei brevemente o que o padrão C diz (começando com C89).

  • Você pode declarar uma função que recebe um número arbitrário de argumentos. I.e. uma função pode ter mais argumentos que parâmetros. Para fazer isso, a lista de seus parâmetros deve terminar com reticências, mas pelo menos um parâmetro fixo [C11 6.9.1 / 8] também deve estar presente:

     void foo(int parm1, int parm2, ...); 
  • As informações sobre o número e os tipos de argumentos correspondentes às reticências não são passadas para a própria função. I.e. após o último parâmetro nomeado ( parm2 no exemplo acima) [C11 6.7.6.3/9] .
  • Para acessar esses argumentos, você deve usar o tipo va_list declarado no cabeçalho <stdarg.h> e 4 macros (3 antes do padrão C11): va_start , va_arg , va_end e va_copy (começando com C11) [C11 7.16] .

    Por exemplo
     int add(int count, ...) { int result = 0; va_list args; va_start(args, count); for (int i = 0; i < count; ++i) { result += va_arg(args, int); } va_end(args); return result; } 

    Sim, a função não sabe quantos argumentos ela possui. Ela precisa de alguma forma passar esse número. Nesse caso, por meio de um único argumento nomeado (outra opção comum é passar NULL como o último argumento, como em execl ou 0).
  • O último argumento nomeado não pode ter uma classe de armazenamento de register , não pode ser uma função ou uma matriz. Caso contrário, comportamento indefinido [C11 7.16.1.4/4] .
  • Além disso, ao último argumento nomeado e a todos os sem nome, é aplicada a " promoção de argumentos padrão " ( promoção de argumentos padrão ; se houver uma boa tradução desse conceito para o russo, eu o uso com prazer). Isso significa que se o argumento tiver o tipo char , short (com ou sem um sinal) ou float , os parâmetros correspondentes deverão ser acessados ​​como int , int (com ou sem um sinal) ou double . Caso contrário, comportamento indefinido [C11 7.16.1.1/2] .
  • Sobre o tipo va_list , va_list -se apenas que ele é declarado em <stdarg.h> e está completo (ou seja, é conhecido o tamanho de um objeto desse tipo) [C11 7.16 / 3] .

Porque Mas porque!


Não há muitos tipos em C. Por que o va_list é declarado no Padrão, mas nada é dito sobre sua estrutura interna?

Por que precisamos de reticências se um número arbitrário de argumentos para uma função pode ser passado através de va_list ? Pode-se dizer agora: “como açúcar sintático”, mas há 40 anos, tenho certeza, não havia tempo para o açúcar.

Philip James Plauger Phillip James Plauger no livro The Standard C library - 1992 - diz que inicialmente o C foi criado exclusivamente para computadores PDP-11. E foi possível classificar todos os argumentos da função usando aritmética simples de ponteiro. O problema apareceu com a popularidade de C e a transferência do compilador para outras arquiteturas. A primeira edição da linguagem de programação The C, de Brian Kernighan e Dennis Ritchie - 1978 - afirma explicitamente:
A propósito, não há maneira aceitável de escrever uma função portátil de um número arbitrário de argumentos, porque Não existe uma maneira portátil para a função chamada descobrir quantos argumentos foram passados ​​para ela quando chamada. ... printf , a função mais típica da linguagem C de um número arbitrário de argumentos, ... não é portátil e deve ser implementada para cada sistema.
Este livro descreve printf , mas ainda não possui vprintf e não menciona o tipo e as macros va_* . Eles aparecem na segunda edição da linguagem de programação C (1988), e esse é o mérito do comitê para o desenvolvimento do primeiro padrão C (C89, também conhecido como ANSI C). O comitê adicionou o <stdarg.h> ao Padrão, tendo como base o <varargs.h> criado por Andrew Koenig para aumentar a portabilidade do SO UNIX. va_* decidido deixar va_* macros va_* como macros, para que fosse mais fácil para os compiladores existentes oferecerem suporte ao novo Padrão.

Agora, com o advento do C89 e da família va_* , tornou-se possível criar funções variáveis ​​portáteis. E embora a estrutura interna dessa família ainda não seja descrita de maneira alguma e não haja requisitos para ela, já está claro o porquê.

Por pura curiosidade, você pode encontrar exemplos da implementação do <stdarg.h> . Por exemplo, a mesma "Biblioteca Padrão C" fornece um exemplo para o Borland Turbo C ++ :

<stdarg.h> de Borland Turbo C ++
 #ifndef _STADARG #define _STADARG #define _AUPBND 1 #define _ADNBND 1 typedef char* va_list #define va_arg(ap, T) \ (*(T*)(((ap) += _Bnd(T, _AUPBND)) - _Bnd(T, _ADNBND))) #define va_end(ap) \ (void)0 #define va_start(ap, A) \ (void)((ap) = (char*)&(A) + _Bnd(A, _AUPBND)) #define _Bnd(X, bnd) \ (sizeof(X) + (bnd) & ~(bnd)) #endif 


A muito mais recente SystemV ABI para AMD64 usa este tipo para va_list :

va_list do SystemV ABI AMD64
 typedef struct { unsigned int gp_offset; unsigned int fp_offset; void *overflow_arg_area; void *reg_save_area; } va_list[1]; 


Em geral, podemos dizer que o tipo e as macros va_* fornecem uma interface padrão para percorrer argumentos de uma função variável, e sua implementação por razões históricas depende do compilador, plataformas de destino e arquitetura. Além disso, uma elipse (isto é, funções variáveis ​​em geral) apareceu em C antes de va_list (isto é, o cabeçalho <stdarg.h> ). E o va_list não foi criado para substituir as reticências, mas para permitir que os desenvolvedores escrevam suas funções variáveis ​​portáteis.

O C ++ mantém amplamente a compatibilidade com versões anteriores do C, portanto, todas as opções acima se aplicam a ele. Mas também há recursos.

Funções variáveis ​​em C ++


O grupo de trabalho WG21 esteve envolvido no desenvolvimento do padrão C ++. Em 1989, o recém-criado C89 Standard foi tomado como base, que mudou gradualmente para descrever o próprio C ++. Em 1995, a proposta N0695 foi recebida de John Micco , na qual o autor sugeriu alterar as restrições para as macros va_* :

  • Porque C ++, ao contrário de C, permite obter o endereço de register de variáveis, então o último argumento nomeado de uma função variável pode ter essa classe de armazenamento.
  • Porque os links que apareceram no C ++ violam a regra não escrita das funções da variável C - o tamanho do parâmetro deve corresponder ao tamanho do tipo declarado -, então o último argumento nomeado não pode ser um link. Caso contrário, comportamento vago.
  • Porque em C ++, não há conceito de " aumentar o tipo de argumento por padrão ", então a frase
    Se o parâmetro parmN for declarado com ... um tipo que não é compatível com o tipo resultante após a aplicação das promoções de argumentos padrão, o comportamento será indefinido
    deve ser substituído por
    Se o parâmetro parmN for declarado com ... um tipo que não é compatível com o tipo resultante ao transmitir um argumento para o qual não há parâmetro, o comportamento será indefinido
Nem traduzi o último ponto para compartilhar minha dor. Primeiro, a “ escalação do tipo de argumento padrão ” no C ++ Standard permanece [C ++ 17 8.2.2 / 9] . E, em segundo lugar, fiquei intrigado por um longo tempo sobre o significado dessa frase, em comparação com o Padrão C, onde tudo está claro. Somente depois de ler o N0695 eu finalmente entendi: quero dizer a mesma coisa.

No entanto, todas as três alterações foram adotadas [C ++ 98 18.7 / 3] . De volta ao C ++, o requisito de uma função variável ter pelo menos um parâmetro nomeado (nesse caso, você não pode acessar os outros, mas mais sobre isso posteriormente) desapareceu, e a lista de tipos válidos de argumentos não nomeados foi complementada com ponteiros para membros da classe e tipos de POD .

O padrão C ++ 03 não trouxe alterações nas funções variacionais. O C ++ 11 começou a converter um argumento sem nome do tipo std::nullptr_t para void* e permitiu que os compiladores, a seu critério, std::nullptr_t tipos com construtores e destruidores não triviais [C ++ 11 5.2.2 / 7] . O C ++ 14 permitiu o uso de funções e matrizes como o último parâmetro nomeado [C ++ 14 18.10 / 3] , e o C ++ 17 proibiu o uso de expansão do pacote de parâmetros ( expansão do pacote ) e variáveis ​​capturadas pela lambda [C ++ 17 21.10.1 / 1]

Como resultado, o C ++ adicionou funções variadas às suas armadilhas. Somente o suporte de tipo não especificado com construtores / destruidores não triviais vale a pena. A seguir, tentarei reduzir todos os recursos não óbvios das funções variáveis ​​em uma lista e complementá-la com exemplos específicos.

Como usar funções variáveis ​​de maneira fácil e incorreta


  1. É incorreto declarar o último argumento nomeado com um tipo promovido, ou seja, char , char signed char , signed char unsigned char , singed short , unsigned short ou float . O resultado de acordo com a Norma será um comportamento indefinido.

    Código inválido
     void foo(float n, ...) { va_list va; va_start(va, n); std::cout << va_arg(va, int) << std::endl; va_end(va); } 


    De todos os compiladores que eu tinha em mãos (gcc, clang, MSVC), apenas o clang emitiu um aviso.

    Clang warning
     ./test.cpp:7:18: warning: passing an object that undergoes default argument promotion to 'va_start' has undefined behavior [-Wvarargs] va_start(va, n); ^ 

    E, embora em todos os casos o código compilado tenha se comportado corretamente, você não deve contar com ele.

    Vai dar certo
     void foo(double n, ...) { va_list va; va_start(va, n); std::cout << va_arg(va, int) << std::endl; va_end(va); } 

  2. É incorreto declarar o último argumento nomeado como referência. Qualquer link. O padrão neste caso também promete um comportamento indefinido.

    Código inválido
     void foo(int& n, ...) { va_list va; va_start(va, n); std::cout << va_arg(va, int) << std::endl; va_end(va); } 

    O gcc 7.3.0 compilou esse código sem um único comentário. O idioma 6.0.0 emitiu um aviso, mas ainda o compilou.

    Clang warning
     ./test.cpp:7:18: warning: passing an object of reference type to 'va_start' has undefined behavior [-Wvarargs] va_start(va, n); ^ 

    Nos dois casos, o programa funcionou corretamente (por sorte, você não pode confiar nele). Mas o MSVC 19.15.26730 se destacou - ele se recusou a compilar o código, porque va_start argumento va_start não va_start ser uma referência.

    Erro do MSVC
     c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.15.26726\include\vadefs.h(151): error C2338: va_start argument must not have reference type and must not be parenthesized 

    Bem, a opção correta se parece, por exemplo, com esta
     void foo(int* n, ...) { va_list va; va_start(va, n); std::cout << va_arg(va, int) << std::endl; va_end(va); } 

  3. É errado solicitar ao va_arg aumentar o tipo - char , short ou float .

    Código inválido
     #include <cstdarg> #include <iostream> void foo(int n, ...) { va_list va; va_start(va, n); std::cout << va_arg(va, int) << std::endl; std::cout << va_arg(va, float) << std::endl; std::cout << va_arg(va, int) << std::endl; va_end(va); } int main() { foo(0, 1, 2.0f, 3); return 0; } 

    É mais interessante aqui. O gcc na compilação avisa que é necessário usar double vez de float , e se esse código ainda for executado, o programa terminará com um erro.

    Aviso do Gcc
     ./test.cpp:9:15: warning: 'float' is promoted to 'double' when passed through '...' std::cout << va_arg(va, float) << std::endl; ^~~~~~ ./test.cpp:9:15: note: (so you should pass 'double' not 'float' to 'va_arg') ./test.cpp:9:15: note: if this code is reached, the program will abort 

    De fato, o programa trava com uma reclamação sobre uma instrução inválida.
    Uma análise de despejo mostra que o programa recebeu um sinal SIGILL. E também mostra a estrutura do va_list . Para 32 bits, isso é

     va = 0xfffc6918 "" 

    isto é va_list é apenas char* . Para 64 bits:

     va = {{gp_offset = 16, fp_offset = 48, overflow_arg_area = 0x7ffef147e7e0, reg_save_area = 0x7ffef147e720}} 

    isto é exatamente o que é descrito em SystemV ABI AMD64.

    clang na compilação alerta para um comportamento indefinido e também sugere a substituição de float por double .

    Clang warning
     ./test.cpp:9:26: warning: second argument to 'va_arg' is of promotable type 'float'; this va_arg has undefined behavior because arguments will be promoted to 'double' [-Wvarargs] std::cout << va_arg(va, float) << std::endl; ^~~~~ 

    Mas o programa não falha mais, a versão de 32 bits produz:

     1 0 1073741824 

    64 bits:

     1 0 3 

    O MSVC produz exatamente os mesmos resultados, apenas sem aviso, mesmo com /Wall .

    Aqui, pode-se supor que a diferença entre 32 e 64 bits se deve ao fato de que, no primeiro caso, a ABI passa todos os argumentos da pilha para a função chamada e, no segundo, os quatro primeiros argumentos (Windows) ou seis (Linux) através do processador registram, o restante através pilha [ wiki ]. Mas não, se você chamar foo não com 4 argumentos, mas com 19, e produzi-los da mesma maneira, o resultado será o mesmo: confusão total na versão de 32 bits e zeros para todos os float na de 64 bits. I.e. o ponto está claro na ABI, mas não no uso de registradores para passar argumentos.

    Bem, é claro, é certo fazê-lo
     void foo(int n, ...) { va_list va; va_start(va, n); std::cout << va_arg(va, int) << std::endl; std::cout << va_arg(va, double) << std::endl; std::cout << va_arg(va, int) << std::endl; va_end(va); } 

  4. É incorreto passar uma instância de uma classe com um construtor ou destruidor não trivial como um argumento sem nome. A menos, é claro, que o destino desse código o excite pelo menos um pouco mais do que "compile e execute aqui e agora".

    Código inválido
     #include <cstdarg> #include <iostream> struct Bar { Bar() { std::cout << "Bar default ctor" << std::endl; } Bar(const Bar&) { std::cout << "Bar copy ctor" << std::endl; } ~Bar() { std::cout << "Bar dtor" << std::endl; } }; struct Cafe { Cafe() { std::cout << "Cafe default ctor" << std::endl; } Cafe(const Cafe&) { std::cout << "Cafe copy ctor" << std::endl; } ~Cafe() { std::cout << "Cafe dtor" << std::endl; } }; void foo(int n, ...) { va_list va; va_start(va, n); std::cout << "Before va_arg" << std::endl; const auto b = va_arg(va, Bar); va_end(va); } int main() { Bar b; Cafe c; foo(1, b, c); return 0; } 

    Clang é o mais rígido de todos novamente. Ele simplesmente se recusa a compilar esse código porque o segundo argumento, va_arg não va_arg tipo POD e alerta que o programa va_arg na inicialização.

    Clang warning
     ./test.cpp:23:31: error: second argument to 'va_arg' is of non-POD type 'Bar' [-Wnon-pod-varargs] const auto b = va_arg(va, Bar); ^~~ ./test.cpp:31:12: error: cannot pass object of non-trivial type 'Bar' through variadic function; call will abort at runtime [-Wnon-pod-varargs] foo(1, b, c); ^ 

    Assim será, se você ainda compilar com o -Wno-non-pod-varargs .

    A MSVC adverte que o uso de tipos com construtores não triviais nesse caso não é portátil.

    Aviso do MSVC
     d:\my documents\visual studio 2017\projects\test\test\main.cpp(31): warning C4840:    "Bar"          

    Mas o código compila e executa corretamente. O seguinte é obtido no console:

    Resultado do lançamento
     Bar default ctor Cafe default ctor Before va_arg Bar copy ctor Bar dtor Cafe dtor Bar dtor 

    I.e. uma cópia é criada apenas no momento da chamada va_arg , e o argumento, ao que parece, é passado por referência. De alguma forma, não é óbvio, mas o Padrão permite.

    O gcc 6.3.0 é compilado sem um único comentário. A saída é a mesma:

    Resultado do lançamento
     Bar default ctor Cafe default ctor Before va_arg Bar copy ctor Bar dtor Cafe dtor Bar dtor 

    O gcc 7.3.0 também não avisa sobre nada, mas o comportamento está mudando:

    Resultado do lançamento
     Bar default ctor Cafe default ctor Cafe copy ctor Bar copy ctor Before va_arg Bar copy ctor Bar dtor Bar dtor Cafe dtor Cafe dtor Bar dtor 

    I.e. essa versão do compilador passa argumentos por valor e, quando chamada, va_arg faz outra cópia. Seria divertido procurar essa diferença ao alternar da sexta para a sétima versão do gcc se os construtores / destruidores tiverem efeitos colaterais.

    A propósito, se você passar e solicitar explicitamente uma referência à classe:

    Outro código errado
     void foo(int n, ...) { va_list va; va_start(va, n); std::cout << "Before va_arg" << std::endl; const auto& b = va_arg(va, Bar&); va_end(va); } int main() { Bar b; Cafe c; foo(1, std::ref(b), c); return 0; } 

    todos os compiladores lançarão um erro. Conforme exigido pela Norma.

    Em geral, se você realmente quiser, é melhor passar argumentos por ponteiro.

    Assim
     void foo(int n, ...) { va_list va; va_start(va, n); std::cout << "Before va_arg" << std::endl; const auto* b = va_arg(va, Bar*); va_end(va); } int main() { Bar b; Cafe c; foo(1, &b, &c); return 0; } 


Resolução de sobrecarga e funções variáveis


Por um lado, tudo é simples: combinar com uma elipse é pior do que combinar com um argumento nomeado regular, mesmo no caso de uma conversão de tipo padrão ou definida pelo usuário.

Exemplo de sobrecarga
 #include <iostream> void foo(...) { std::cout << "C variadic function" << std::endl; } void foo(int) { std::cout << "Ordinary function" << std::endl; } int main() { foo(1); foo(1ul); foo(); return 0; } 


Resultado do lançamento
 $ ./test Ordinary function Ordinary function C variadic function 

Mas isso só funciona até que a chamada para foo sem argumentos precise ser considerada separadamente.

Chame foo sem argumentos
 #include <iostream> void foo(...) { std::cout << "C variadic function" << std::endl; } void foo() { std::cout << "Ordinary function without arguments" << std::endl; } int main() { foo(1); foo(); return 0; } 

Saída do compilador
 ./test.cpp:16:9: error: call of overloaded 'foo()' is ambiguous foo(); ^ ./test.cpp:3:6: note: candidate: void foo(...) void foo(...) ^~~ ./test.cpp:8:6: note: candidate: void foo() void foo() ^~~ 

Tudo está de acordo com o Padrão: não há argumentos - não há comparação com as elipses e, quando a sobrecarga é resolvida, a função variativa não se torna pior que a usual.

Quando vale a pena usar funções variáveis


Bem, as funções variativas às vezes não se comportam de maneira muito óbvia e, no contexto do C ++, podem facilmente ser pouco portáteis. Existem muitas dicas na Internet como “Não crie ou use funções variáveis ​​de C”, mas elas não removerão seu suporte do C ++ Standard. Portanto, há algum benefício nesses recursos? Bem aí.

  • O caso mais comum e óbvio é a compatibilidade com versões anteriores. Aqui, incluirei o uso de bibliotecas C de terceiros (meu caso com JNI) e o fornecimento da API C para a implementação C ++.
  • SFINAE . É muito útil aqui que, em C ++, não é necessário que uma função variável tenha argumentos nomeados e que, ao resolver funções sobrecarregadas, uma função variável é considerada a última (se houver pelo menos um argumento). E, como qualquer outra função, uma função variável só pode ser declarada, mas nunca chamada.

    Exemplo
     template <class T> struct HasFoo { private: template <class U, class = decltype(std::declval<U>().foo())> static void detect(const U&); static int detect(...); public: static constexpr bool value = std::is_same<void, decltype(detect(std::declval<T>()))>::value; }; 

    Embora no C ++ 14 você possa fazer um pouco diferente.

    Outro exemplo
     template <class T> struct HasFoo { private: template <class U, class = decltype(std::declval<U>().foo())> static constexpr bool detect(const U*) { return true; } template <class U> static constexpr bool detect(...) { return false; } public: static constexpr bool value = detect<T>(nullptr); }; 

    E, neste caso, já é necessário observar com que argumentos detect(...) podem ser chamados. Eu preferiria mudar algumas linhas e usar uma alternativa moderna a funções variáveis, desprovida de todas as suas deficiências.

Modelos de variantes ou como criar funções a partir de um número arbitrário de argumentos no C ++ moderno


A ideia de modelos variáveis ​​foi proposta por Douglas Gregor, Jaakko Järvi e Gary Powell em 2004, ou seja, 7 anos antes da adoção do padrão C ++ 11, no qual esses modelos de variáveis ​​eram oficialmente suportados.A Norma incluiu uma terceira revisão de sua proposta, N2080 .

Desde o início, modelos variáveis ​​foram criados para que os programadores tivessem a oportunidade de criar funções seguras para tipos (e portáteis!) A partir de um número arbitrário de argumentos. Outro objetivo é simplificar o suporte a modelos de classe com um número variável de parâmetros, mas agora estamos falando apenas de funções variáveis.

Modelos de variáveis ​​trouxeram três novos conceitos para o C ++ [C ++ 17 17.5.3] :

  • parâmetros do modelo de pacote ( template parâmetro pacote ) - é um modelo de parâmetro, em vez de que é possível transferir qualquer (incluindo 0) número de argumento modelo;
  • um pacote de parâmetros de função (pacote de parâmetros de função ) - portanto, este é um parâmetro de função que aceita qualquer número (incluindo 0) de argumentos de função;
  • e a expansão do pacote ( expansão do pacote ) é a única coisa que pode ser feita com o pacote de parâmetros.

Exemplo
 template <class ... Args> void foo(const std::string& format, Args ... args) { printf(format.c_str(), args...); } 

class ... Args — , Args ... args — , args... — .

Uma lista completa de onde e como os pacotes de parâmetros podem ser expandidos é fornecida no próprio Padrão [C ++ 17 17.5.3 / 4] . E no contexto da discussão de funções variáveis, basta dizer que:

  • O pacote de parâmetros da função pode ser expandido para a lista de argumentos de outra função
     template <class ... Args> void bar(const std::string& format, Args ... args) { foo<Args...>(format.c_str(), args...); } 

  • ou para a lista de inicialização
     template <class ... Args> void foo(const std::string& format, Args ... args) { const auto list = {args...}; } 

  • ou para a lista de captura lambda
     template <class ... Args> void foo(const std::string& format, Args ... args) { auto lambda = [&format, args...] () { printf(format.c_str(), args...); }; lambda(); } 

  • outro pacote de parâmetros de função pode ser expandido em uma expressão de convolução
     template <class ... Args> int foo(Args ... args) { return (0 + ... + args); } 

    As convoluções apareceram em C ++ 14 e podem ser unárias e binárias, direita e esquerda. A descrição mais completa, como sempre, está no Padrão [C ++ 17 8.1.6] .
  • os dois tipos de pacotes de parâmetros podem ser expandidos para sizeof ... operator
     template <class ... Args> void foo(Args ... args) { const auto size1 = sizeof...(Args); const auto size2 = sizeof...(args); } 


Na divulgação do pacote reticências explícita é necessária para apoiar os vários modelos ( padrões ) a divulgação e para evitar essa ambigüidade.

Por exemplo
 template <class ... Args> void foo() { using OneTuple = std::tuple<std::tuple<Args>...>; using NestTuple = std::tuple<std::tuple<Args...>>; } 

OneTuple — ( std:tuple<std::tuple<int>>, std::tuple<double>> ), NestTuple — , — ( std::tuple<std::tuple<int, double>> ).

Exemplo de implementação de printf usando modelos variáveis


Como já mencionei, os modelos de variáveis ​​também foram criados como substitutos diretos das funções variáveis ​​de C. Os autores desses modelos propuseram sua versão muito simples, mas segura para o tipo printf- uma das primeiras funções variáveis ​​do C.

printf em modelos
 void printf(const char* s) { while (*s) { if (*s == '%' && *++s != '%') throw std::runtime_error("invalid format string: missing arguments"); std::cout << *s++; } } template <typename T, typename ... Args> void printf(const char* s, T value, Args ... args) { while (*s) { if (*s == '%' && *++s != '%') { std::cout << value; return printf(++s, args...); } std::cout << *s++; } throw std::runtime_error("extra arguments provided to printf"); } 

Eu suspeito, então esse padrão de enumeração de argumentos variáveis ​​apareceu - através de uma chamada recursiva de funções sobrecarregadas. Mas ainda prefiro a opção sem recursão.

printf em modelos e sem recursão
 template <typename ... Args> void printf(const std::string& fmt, const Args& ... args) { size_t fmtIndex = 0; size_t placeHolders = 0; auto printFmt = [&fmt, &fmtIndex, &placeHolders]() { for (; fmtIndex < fmt.size(); ++fmtIndex) { if (fmt[fmtIndex] != '%') std::cout << fmt[fmtIndex]; else if (++fmtIndex < fmt.size()) { if (fmt[fmtIndex] == '%') std::cout << '%'; else { ++fmtIndex; ++placeHolders; break; } } } }; ((printFmt(), std::cout << args), ..., (printFmt())); if (placeHolders < sizeof...(args)) throw std::runtime_error("extra arguments provided to printf"); if (placeHolders > sizeof...(args)) throw std::runtime_error("invalid format string: missing arguments"); } 

Resolução de sobrecarga e funções de modelo variável


Na resolução, essas funções variativas são consideradas, depois de outras, como padrão e menos especializadas. Mas não há problema no caso de uma chamada sem argumentos.

Exemplo de sobrecarga
 #include <iostream> void foo(int) { std::cout << "Ordinary function" << std::endl; } void foo() { std::cout << "Ordinary function without arguments" << std::endl; } template <class T> void foo(T) { std::cout << "Template function" << std::endl; } template <class ... Args> void foo(Args ...) { std::cout << "Template variadic function" << std::endl; } int main() { foo(1); foo(); foo(2.0); foo(1, 2); return 0; } 

Resultado do lançamento
 $ ./test Ordinary function Ordinary function without arguments Template function Template variadic function 

Quando a sobrecarga é resolvida, uma função de modelo variável pode ignorar apenas uma função C variável (embora por que misturá-las?). Exceto - é claro! - ligar sem argumentos.

Ligar sem argumentos
 #include <iostream> void foo(...) { std::cout << "C variadic function" << std::endl; } template <class ... Args> void foo(Args ...) { std::cout << "Template variadic function" << std::endl; } int main() { foo(1); foo(); return 0; } 

Resultado do lançamento
 $ ./test Template variadic function C variadic function 

Há uma comparação com uma elipse - a função correspondente perde, não há comparação com uma elipse - e a função de modelo é inferior à não-modelo.

Uma observação rápida sobre a velocidade das funções de modelo variável


Em 2008, Loïc Joly enviou sua proposta N2772 ao Comitê de Padronização do C ++ , no qual mostrou na prática que funções de modelo variável funcionam mais lentamente que funções semelhantes, cujo argumento é a lista de inicialização ( std::initializer_list). E, embora isso contradisse as justificativas teóricas do próprio autor, Joli propôs implementá-lo std::min, std::maxe std::minmaxfoi com a ajuda de listas de inicialização, e não de modelos variáveis.

Mas já em 2009, uma refutação apareceu. Nos testes de Joli, um "erro grave" foi descoberto (parece até para si mesmo). Novos testes (veja aqui e aqui) mostrou que as funções de modelo variável ainda são mais rápidas e, às vezes, significativamente. O que não é surpreendente, já que a lista de inicialização faz cópias de seus elementos e, para modelos variáveis, você pode contar muito no estágio de compilação.

No entanto, no C ++ 11 e nos padrões subseqüentes std::min, std::maxe std::minmaxsão funções de modelo comuns, um número arbitrário de argumentos aos quais são passados ​​pela lista de inicialização.

Breve resumo e conclusão


Portanto, funções variáveis ​​no estilo C:

  • Eles não sabem o número de seus argumentos ou seus tipos. O desenvolvedor deve usar parte dos argumentos da função para passar informações sobre o restante.
  • Aumente implicitamente os tipos de argumentos não nomeados (e o último nomeado). Se você se esquecer disso, terá um comportamento vago.
  • Eles mantêm compatibilidade com o C puro e, portanto, não suportam a passagem de argumentos por referência.
  • Antes do C ++ 11, os argumentos que não eram do tipo POD não eram suportados e, desde o C ++ 11, o suporte para tipos não triviais era deixado a critério do compilador. I.e. O comportamento do código depende do compilador e de sua versão.

O único uso permitido de funções variáveis ​​é interagir com a API C no código C ++. Para todo o resto, incluindo SFINAE , existem funções de modelo variável que:

  • Conheça o número e os tipos de todos os seus argumentos.
  • Digite safe, não altere os tipos de seus argumentos.
  • Eles suportam a passagem de argumentos de qualquer forma - por valor, por ponteiro, por referência, por link universal.
  • Como qualquer outra função C ++, não há restrições nos tipos de argumentos.
  • ( C ), .

As funções de modelo variável podem ser mais detalhadas em comparação com suas contrapartes no estilo C e, às vezes, até exigem sua própria versão não-modelo sobrecarregada (passagem de argumento recursivo). Eles são mais difíceis de ler e escrever. Mas tudo isso é mais do que pago pela ausência das deficiências listadas e pela presença das vantagens listadas.

Bem, a conclusão é simples: as funções variadas no estilo C permanecem no C ++ apenas por causa da compatibilidade com versões anteriores, e oferecem uma ampla gama de opções para fotografar sua perna. No C ++ moderno, é altamente recomendável não escrever novos e, se possível, não usar funções C variáveis ​​existentes. As funções de modelo variável pertencem ao mundo do C ++ moderno e são muito mais seguras. Use-os.

Literatura e Fontes



PS


É fácil encontrar e baixar versões eletrônicas dos livros mencionados na rede. Mas não tenho certeza de que será legal, então não dou links.

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


All Articles