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
CallVoidMethod
há
CallVoidMethodV
, 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).
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 porSe 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
- É 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); }
- É 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); }
- É 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); }
- É 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::max
e std::minmax
foi 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::max
e std::minmax
sã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.