C ++: une session d'archéologie spontanée et pourquoi vous ne devriez pas utiliser des fonctions variables dans le style de C

Tout a commencé, comme d'habitude, avec une erreur. C'est la première fois que je travaille avec Java Native Interface et dans la partie C ++, j'ai encapsulé une fonction qui crée un objet Java. Cette fonction - CallVoidMethod - est variable, c'est-à-dire en plus d'un pointeur sur l'environnement JNI , un pointeur sur le type d'objet à créer et un identifiant pour la méthode appelée (dans ce cas, le constructeur), il prend un nombre arbitraire d'autres arguments. Ce qui est logique, car ces autres arguments sont passés à la méthode appelée du côté Java, et les méthodes peuvent être différentes, avec un nombre différent d'arguments de n'importe quel type.

En conséquence, j'ai également créé ma variable wrapper. Pour passer un nombre arbitraire d'arguments à CallVoidMethod utilisé va_list , car c'est différent dans ce cas. Oui, c'est ce que va_list envoyé à CallVoidMethod . Et abandonné la faille de segmentation banale de la JVM.

En 2 heures, j'ai réussi à essayer plusieurs versions de la JVM, du 8 au 11, car: premièrement, c'est ma première expérience avec la JVM , et dans ce domaine j'ai fait plus confiance à StackOverflow qu'à moi, et deuxièmement, quelqu'un puis sur StackOverflow j'ai conseillé dans ce cas d'utiliser non OpenJDK, mais OracleJDK, et non 8, mais 10. Et seulement alors j'ai finalement remarqué qu'en plus de la variable CallVoidMethod il y a CallVoidMethodV , qui prend un nombre arbitraire d'arguments via va_list .

Ce que je n'ai pas aimé le plus dans cette histoire, c'est que je n'ai pas immédiatement remarqué la différence entre les points de suspension (ellipses) et va_list . Et après avoir remarqué, je ne pouvais pas m'expliquer quelle était la différence fondamentale. Donc, nous devons traiter les points de suspension, et va_list , et (puisque nous parlons toujours de C ++) avec des modèles de variables.

Qu'en est-il des points de suspension et de la va_list dans la norme


La norme C ++ décrit uniquement les différences entre ses exigences et celles de la norme C. Les différences elles-mêmes seront discutées plus tard, mais pour l'instant j'expliquerai brièvement ce que dit la norme C (en commençant par C89).

  • Vous pouvez déclarer une fonction qui prend un nombre arbitraire d'arguments. C'est-à-dire une fonction peut avoir plus d'arguments que de paramètres. Pour ce faire, la liste de ses paramètres doit se terminer par des points de suspension, mais au moins un paramètre fixe [C11 6.9.1 / 8] doit également être présent:

     void foo(int parm1, int parm2, ...); 
  • Les informations sur le nombre et les types d'arguments correspondant aux points de suspension ne sont pas transmises à la fonction elle-même. C'est-à-dire après le dernier paramètre nommé ( parm2 dans l'exemple ci-dessus) [C11 6.7.6.3/9] .
  • Pour accéder à ces arguments, vous devez utiliser le type va_list déclaré dans l'en-tête <stdarg.h> et 4 macros (3 avant la norme C11): va_start , va_arg , va_end et va_copy (commençant par C11) [C11 7.16] .

    Par exemple
     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; } 

    Oui, la fonction ne sait pas combien d'arguments elle possède. Elle doit en quelque sorte transmettre ce numéro. Dans ce cas, via un seul argument nommé (une autre option courante consiste à passer NULL comme dernier argument, comme dans execl ou 0).
  • Le dernier argument nommé ne peut pas avoir de classe de stockage de register ; il ne peut pas être une fonction ou un tableau. Sinon, comportement indéfini [C11 7.16.1.4/4] .
  • De plus, au dernier argument nommé et à tous ceux sans nom, la « promotion d'argument par défaut » est appliquée ( promotion d'argument par défaut ; s'il y a une bonne traduction de ce concept en russe, je l'utilise volontiers). Cela signifie que si l'argument a le type char , short (avec ou sans signe) ou float , alors les paramètres correspondants doivent être accessibles comme int , int (avec ou sans signe) ou double . Sinon, comportement indéfini [C11 7.16.1.1/2] .
  • Concernant le type va_list on dit seulement qu'il est déclaré dans <stdarg.h> et qu'il est complet (c'est-à-dire que la taille d'un objet de ce type est connue) [C11 7.16 / 3] .

Pourquoi? Mais parce que!


Il n'y a pas beaucoup de types en C. Pourquoi va_list est- va_list déclaré dans la norme, mais rien n'est dit sur sa structure interne?

Pourquoi avons-nous besoin d'une ellipse si un nombre arbitraire d'arguments à une fonction peut être passé via va_list ? On pourrait dire maintenant: "comme sucre syntaxique", mais il y a 40 ans, j'en suis sûr, il n'y avait pas de temps pour le sucre.

Philip James Plauger Phillip James Plauger dans le livre The Standard C library - 1992 - dit qu'initialement C a été créé exclusivement pour les ordinateurs PDP-11. Et là, il était possible de trier tous les arguments de la fonction en utilisant une arithmétique de pointeur simple. Le problème est apparu avec la popularité de C et le transfert du compilateur vers d'autres architectures. La première édition du langage de programmation C de Brian Kernighan et Dennis Ritchie - 1978 - déclare explicitement:
Soit dit en passant, il n'existe aucun moyen acceptable d'écrire une fonction portable d'un nombre arbitraire d'arguments, car Il n'existe aucun moyen portable pour la fonction appelée de savoir combien d'arguments lui ont été transmis lors de son appel. ... printf , la fonction en langage C la plus typique d'un nombre arbitraire d'arguments, ... n'est pas portable et doit être implémentée pour chaque système.
Ce livre décrit printf , mais n'a pas encore vprintf , et ne mentionne pas le type et les macros va_* . Ils apparaissent dans la deuxième édition du langage de programmation C (1988), et c'est le mérite du comité pour le développement de la première norme C (C89, alias ANSI C). Le comité a ajouté le titre <stdarg.h> à la norme, en prenant comme base <varargs.h> , créé par Andrew Koenig dans le but d'augmenter la portabilité du système d'exploitation UNIX. va_* été décidé de laisser les macros va_* tant que macros afin qu'il soit plus facile pour les compilateurs existants de prendre en charge la nouvelle norme.

Maintenant, avec l'avènement de C89 et de la famille va_* , il est devenu possible de créer des fonctions variables portables. Et bien que la structure interne de cette famille ne soit encore décrite d'aucune façon, et qu'il n'y ait aucune exigence à cet égard, il est déjà clair pourquoi.

Par pure curiosité, vous pouvez trouver des exemples de mise en œuvre de <stdarg.h> . Par exemple, la même «bibliothèque standard C» fournit un exemple pour 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 


L' ABI SystemV beaucoup plus récent pour AMD64 utilise ce type pour va_list :

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


En général, nous pouvons dire que le type et les macros va_* fournissent une interface standard pour parcourir les arguments d'une fonction variable, et leur implémentation pour des raisons historiques dépend du compilateur, des plates-formes cibles et de l'architecture. De plus, une ellipse (c'est-à-dire les fonctions variables en général) est apparue en C plus tôt que va_list (c'est-à-dire l'en-tête <stdarg.h> ). Et va_list n'a pas été créé pour remplacer les points de suspension, mais pour permettre aux développeurs d'écrire leurs fonctions variables portables.

C ++ maintient largement la compatibilité descendante avec C, donc tout ce qui précède s'applique à lui. Mais il y a aussi des fonctionnalités.

Fonctions variables en C ++


Le groupe de travail WG21 a été impliqué dans le développement de la norme C ++. En 1989, la nouvelle norme C89 a été prise comme base, qui a progressivement changé pour décrire le C ++ lui-même. En 1995, la proposition N0695 a été reçue de John Micco , dans laquelle l'auteur a suggéré de changer les restrictions pour les macros va_* :

  • Parce que C ++, contrairement à C, vous permet d'obtenir l'adresse de register des variables, puis le dernier argument nommé d'une fonction variable peut avoir cette classe de stockage.
  • Parce que les liens apparus en C ++ violent la règle non écrite des fonctions variables C - la taille du paramètre doit correspondre à la taille de son type déclaré - alors le dernier argument nommé ne peut pas être un lien. Sinon, comportement vague.
  • Parce que en C ++ il n'y a pas de concept de " rehaussement du type de l'argument par défaut ", alors la phrase
    Si le paramètre parmN est déclaré avec ... un type qui n'est pas compatible avec le type qui en résulte après application des promotions d'argument par défaut, le comportement n'est pas défini
    doit être remplacé par
    Si le paramètre parmN est déclaré avec ... un type qui n'est pas compatible avec le type qui en résulte lors du passage d'un argument pour lequel il n'y a pas de paramètre, le comportement n'est pas défini
Je n'ai même pas traduit le dernier point afin de partager ma douleur. Premièrement, l' escalade du type d'argument par défaut dans C ++ Standard reste [C ++ 17 8.2.2 / 9] . Et deuxièmement, je me suis longtemps interrogé sur le sens de cette phrase, par rapport à la norme C, où tout est clair. Ce n'est qu'après avoir lu N0695 que j'ai finalement compris: je veux dire la même chose.

Cependant, les 3 modifications ont été adoptées [C ++ 98 18.7 / 3] . De retour en C ++, l'exigence d'une fonction variable d'avoir au moins un paramètre nommé (dans ce cas, vous ne pouvez pas accéder aux autres, mais plus sur cela plus tard) a disparu, et la liste des types valides d'arguments sans nom a été complétée par des pointeurs vers les membres de la classe et les types POD .

La norme C ++ 03 n'a apporté aucun changement aux fonctions variationnelles. C ++ 11 a commencé à convertir un argument sans nom de type std::nullptr_t en void* et a permis aux compilateurs, à leur discrétion, de prendre en charge des types avec des constructeurs et des destructeurs non triviaux [C ++ 11 5.2.2 / 7] . C ++ 14 autorisait l'utilisation de fonctions et de tableaux comme dernier paramètre nommé [C ++ 14 18.10 / 3] , et C ++ 17 interdisait l'utilisation de packs d'extension et de variables capturés par le lambda [C ++ 17 21.10.1 / 1] .

En conséquence, C ++ a ajouté des fonctions variées à ses pièges. Seul le support de type non spécifié avec des constructeurs / destructeurs non triviaux en vaut la peine. Ci-dessous, je vais essayer de réduire toutes les fonctionnalités non évidentes des fonctions variables en une seule liste et de la compléter avec des exemples spécifiques.

Comment utiliser les fonctions variables facilement et incorrectement


  1. Il est incorrect de déclarer le dernier argument nommé avec un type promu, c'est-à-dire char , caractère signed char , caractère unsigned char , singed short unsigned short ou float . Le résultat selon la norme sera un comportement indéfini.

    Code invalide
     void foo(float n, ...) { va_list va; va_start(va, n); std::cout << va_arg(va, int) << std::endl; va_end(va); } 


    De tous les compilateurs que j'avais sous la main (gcc, clang, MSVC), seul clang a émis un avertissement.

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

    Et bien que dans tous les cas le code compilé se soit comporté correctement, vous ne devez pas compter dessus.

    Ce sera juste
     void foo(double n, ...) { va_list va; va_start(va, n); std::cout << va_arg(va, int) << std::endl; va_end(va); } 

  2. Il est incorrect de déclarer le dernier argument nommé comme référence. Tout lien. La norme dans ce cas promet également un comportement indéfini.

    Code invalide
     void foo(int& n, ...) { va_list va; va_start(va, n); std::cout << va_arg(va, int) << std::endl; va_end(va); } 

    gcc 7.3.0 a compilé ce code sans un seul commentaire. lang 6.0.0 a émis un avertissement, mais l'a quand même compilé.

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

    Dans les deux cas, le programme a fonctionné correctement (heureusement, vous ne pouvez pas vous y fier). Mais MSVC 19.15.26730 s'est distingué - il a refusé de compiler le code, car va_start argument va_start ne va_start pas être une référence.

    Erreur de 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 

    Eh bien, l'option correcte ressemble, par exemple, à ceci
     void foo(int* n, ...) { va_list va; va_start(va, n); std::cout << va_arg(va, int) << std::endl; va_end(va); } 

  3. Il est faux de demander à va_arg augmenter le type - char , short ou float .

    Code invalide
     #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; } 

    C'est plus intéressant ici. gcc lors de la compilation donne un avertissement qu'il est nécessaire d'utiliser double au lieu de float , et si ce code est toujours exécuté, le programme se terminera avec une erreur.

    Avertissement 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 

    En effet, le programme plante avec une plainte concernant une instruction invalide.
    Une analyse de vidage montre que le programme a reçu un signal SIGILL. Et il montre également la structure de va_list . Pour 32 bits, c'est

     va = 0xfffc6918 "" 

    c'est-à-dire va_list est juste char* . Pour 64 bits:

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

    c'est-à-dire exactement ce qui est décrit dans SystemV ABI AMD64.

    clang lors de la compilation met en garde contre un comportement indéfini et suggère également de remplacer float par double .

    Avertissement Clang
     ./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; ^~~~~ 

    Mais le programme ne plante plus, la version 32 bits produit:

     1 0 1073741824 

    64 bits:

     1 0 3 

    MSVC produit exactement les mêmes résultats, uniquement sans avertissement, même avec /Wall .

    Ici, on pourrait supposer que la différence entre 32 et 64 bits est due au fait que dans le premier cas, l'ABI transmet tous les arguments via la pile à la fonction appelée, et dans le second, les quatre premiers (Windows) ou six (Linux) arguments via les registres du processeur, le reste via pile [ wiki ]. Mais non, si vous appelez foo non pas avec 4 arguments, mais avec 19, et les sortez de la même manière, le résultat sera le même: un gâchis complet dans la version 32 bits, et des zéros pour tous les float dans le 64 bits. C'est-à-dire le point est bien sûr dans ABI, mais pas dans l'utilisation de registres pour passer des arguments.

    Eh bien, bien sûr, c'est juste de le faire
     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. Il est incorrect de passer une instance d'une classe avec un constructeur ou destructeur non trivial comme argument sans nom. À moins, bien sûr, que le sort de ce code ne vous excite au moins un peu plus que «compiler et exécuter ici et maintenant».

    Code invalide
     #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 est le plus strict de tous. Il refuse simplement de compiler ce code car le deuxième argument, va_arg pas un type POD, et avertit que le programme va va_arg au démarrage.

    Avertissement Clang
     ./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); ^ 

    Il en sera ainsi si vous compilez toujours avec l' -Wno-non-pod-varargs .

    MSVC avertit que l'utilisation de types avec des constructeurs non triviaux dans ce cas n'est pas portable.

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

    Mais le code se compile et s'exécute correctement. Les éléments suivants sont obtenus dans la console:

    Résultat de lancement
     Bar default ctor Cafe default ctor Before va_arg Bar copy ctor Bar dtor Cafe dtor Bar dtor 

    C'est-à-dire une copie n'est créée qu'au moment de l'appel de va_arg , et l'argument, il s'avère, est passé par référence. Ce n'est pas évident, mais la norme le permet.

    gcc 6.3.0 compile sans un seul commentaire. La sortie est la même:

    Résultat de lancement
     Bar default ctor Cafe default ctor Before va_arg Bar copy ctor Bar dtor Cafe dtor Bar dtor 

    gcc 7.3.0 ne prévient également de rien, mais le comportement change:

    Résultat de lancement
     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 

    C'est-à-dire cette version du compilateur transmet les arguments par valeur, et lorsqu'elle est appelée, va_arg fait une autre copie. Il serait amusant de rechercher cette différence lors du passage de la 6e à la 7e version de gcc si les constructeurs / destructeurs ont des effets secondaires.

    Au fait, si vous passez explicitement et demandez une référence à la classe:

    Un autre mauvais code
     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; } 

    alors tous les compilateurs lèveront une erreur. Tel que requis par la norme.

    En général, si vous le voulez vraiment, il est préférable de passer des arguments par pointeur.

    Comme ça
     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; } 


Résolution de surcharge et fonctions variables


D'une part, tout est simple: la correspondance avec des points de suspension est pire que la correspondance avec un argument nommé régulier, même dans le cas d'une conversion de type standard ou définie par l'utilisateur.

Exemple de surcharge
 #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; } 


Résultat de lancement
 $ ./test Ordinary function Ordinary function C variadic function 

Mais cela ne fonctionne que jusqu'à ce que l'appel à foo sans arguments doive être considéré séparément.

Appelez foo sans arguments
 #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; } 

Sortie du compilateur
 ./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() ^~~ 

Tout est conforme à la norme: il n'y a pas d'arguments - il n'y a pas de comparaison avec les points de suspension, et lorsque la surcharge est résolue, la fonction variative ne devient pas pire que l'habituelle.

Quand vaut-il quand même la peine d'utiliser des fonctions variables


Eh bien, les fonctions variées ne se comportent parfois pas de manière très évidente et dans le contexte du C ++ peuvent facilement se révéler mal portables. Il existe de nombreux conseils sur Internet comme «Ne pas créer ou utiliser de fonctions C variables», mais ils ne supprimeront pas leur prise en charge de la norme C ++. Il y a donc des avantages à ces fonctionnalités? Et bien là.

  • Le cas le plus courant et le plus évident est la compatibilité descendante. Ici, j'inclurai à la fois l'utilisation de bibliothèques C tierces (mon cas avec JNI) et la fourniture de l'API C à l'implémentation C ++.
  • SFINAE . Ici, il est très utile qu’en C ++ une fonction variable ne doive pas avoir d’arguments nommés et que lors de la résolution de fonctions surchargées, une fonction variable soit considérée en dernier (s’il existe au moins un argument). Et comme toute autre fonction, une fonction variable ne peut être déclarée, mais jamais appelée.

    Exemple
     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; }; 

    Bien qu'en C ++ 14, vous pouvez faire un peu différemment.

    Un autre exemple
     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); }; 

    Et dans ce cas, il est déjà nécessaire de regarder avec quels arguments detect(...) peut être appelé. Je préférerais changer quelques lignes et utiliser une alternative moderne aux fonctions variables, dépourvue de tous leurs défauts.

Modèles de variantes ou comment créer des fonctions à partir d'un nombre arbitraire d'arguments en C ++ moderne


L'idée de modèles variables a été proposée par Douglas Gregor, Jaakko Järvi et Gary Powell en 2004, c'est-à-dire 7 ans avant l'adoption de la norme C ++ 11, dans laquelle ces modèles de variables étaient officiellement pris en charge.La norme comprenait une troisième révision de leur proposition, N2080 .

Dès le début, des modèles de variables ont été créés afin que les programmeurs aient la possibilité de créer des fonctions de type sécurisé (et portables!) À partir d'un nombre arbitraire d'arguments. Un autre objectif est de simplifier la prise en charge des modèles de classe avec un nombre variable de paramètres, mais maintenant nous ne parlons que des fonctions variables.

Les modèles de variables ont apporté trois nouveaux concepts au C ++ [C ++ 17 17.5.3] :

  • ensemble de paramètres de modèle ( pack de paramètre de modèle ) - est un modèle de paramètre, au lieu de laquelle il est possible de transférer tout (y compris 0) nombre d'argument de modèle;
  • un ensemble de paramètres de fonction ( pack de paramètres de fonction ) - en conséquence, il s'agit d'un paramètre de fonction qui prend n'importe quel nombre (y compris 0) d'arguments de fonction;
  • et l'expansion du package ( extension du pack ) est la seule chose qui peut être faite avec le package de paramètres.

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

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

Une liste complète où et comment les packages de paramètres peuvent être développés est donnée dans la norme elle-même [C ++ 17 17.5.3 / 4] . Et dans le cadre de la discussion des fonctions variables, il suffit de dire que:

  • le package de paramètres de fonction peut être développé dans la liste d'arguments d'une autre fonction
     template <class ... Args> void bar(const std::string& format, Args ... args) { foo<Args...>(format.c_str(), args...); } 

  • ou à la liste d'initialisation
     template <class ... Args> void foo(const std::string& format, Args ... args) { const auto list = {args...}; } 

  • ou à la liste de capture lambda
     template <class ... Args> void foo(const std::string& format, Args ... args) { auto lambda = [&format, args...] () { printf(format.c_str(), args...); }; lambda(); } 

  • un autre ensemble de paramètres de fonction peut être développé dans une expression de convolution
     template <class ... Args> int foo(Args ... args) { return (0 + ... + args); } 

    Les convolutions sont apparues en C ++ 14 et peuvent être unaires et binaires, droite et gauche. La description la plus complète, comme toujours, se trouve dans la norme [C ++ 17 8.1.6] .
  • les deux types de packages de paramètres peuvent être étendus en opérateur sizeof ...
     template <class ... Args> void foo(Args ... args) { const auto size1 = sizeof...(Args); const auto size2 = sizeof...(args); } 


En divulguant le paquet explicite de points de suspension est nécessaire pour soutenir les différents modèles ( modèles ) et la communication pour éviter cette ambiguïté.

Par exemple
 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>> ).

Exemple d'implémentation de printf à l'aide de modèles de variables


Comme je l'ai déjà mentionné, des modèles de variables ont également été créés en remplacement direct des fonctions variables de C. Les auteurs de ces modèles ont eux-mêmes proposé leur version très simple mais sécurisée printf- l'une des premières fonctions variables en C.

printf sur les modèles
 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"); } 

Je soupçonne que ce schéma d'énumération d'arguments variables est apparu - à travers un appel récursif de fonctions surchargées. Mais je préfère toujours l'option sans récursivité.

printf sur des modèles et sans récursivité
 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"); } 

Résolution de surcharge et fonctions de modèle variables


Lors de la résolution, ces fonctions variables sont considérées, après d'autres, comme étant standard et les moins spécialisées. Mais il n'y a pas de problème dans le cas d'un appel sans argument.

Exemple de surcharge
 #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; } 

Résultat de lancement
 $ ./test Ordinary function Ordinary function without arguments Template function Template variadic function 

Lorsque la surcharge est résolue, une fonction de modèle variable ne peut contourner qu'une fonction C variable (bien que pourquoi les mélanger?). Sauf - bien sûr! - appel sans arguments.

Appel sans arguments
 #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; } 

Résultat de lancement
 $ ./test Template variadic function C variadic function 

Il y a une comparaison avec des points de suspension - la fonction correspondante perd, il n'y a pas de comparaison avec des points de suspension - et la fonction modèle est inférieure à la fonction non modèle.

Une note rapide sur la vitesse des fonctions de modèles variables


En 2008, Loïc Joly a soumis sa proposition N2772 au Comité de normalisation C ++ , dans lequel il a montré dans la pratique que les fonctions de modèle variable fonctionnent plus lentement que les fonctions similaires, dont l'argument est la liste d'initialisation ( std::initializer_list). Et bien que cela contredit les justifications théoriques de l'auteur lui-même, Joli a proposé de le mettre en œuvre std::min, std::maxet std::minmaxprécisément à l'aide de listes d'initialisation, et non avec des modèles variables.

Mais déjà en 2009, une réfutation est apparue. Lors des tests de Joli, une «grave erreur» a été découverte (semble-t-il, même pour lui-même). Nouveaux tests (voir ici et ici) ont montré que les fonctions de modèle variables sont encore plus rapides, et parfois significatives. Ce qui n'est pas surprenant puisque la liste d'initialisation fait des copies de ses éléments, et pour les modèles de variables, vous pouvez compter beaucoup au stade de la compilation.

Néanmoins, en C ++ 11 et les normes suivantes std::min, std::maxet std::minmaxsont des fonctions de modèle ordinaires, un nombre arbitraire d'arguments à qui sont passés à travers la liste d'initialisation.

Bref résumé et conclusion


Ainsi, les fonctions variables de style C:

  • Ils ne connaissent ni le nombre de leurs arguments ni leurs types. Le développeur doit utiliser une partie des arguments de la fonction afin de transmettre des informations sur le reste.
  • Augmentez implicitement les types d'arguments sans nom (et le dernier nommé). Si vous l'oubliez, vous obtenez un comportement vague.
  • Ils conservent une compatibilité descendante avec le C pur et ne prennent donc pas en charge le passage d'arguments par référence.
  • Avant C ++ 11, les arguments non de type POD n'étaient pas pris en charge , et depuis C ++ 11, la prise en charge des types non triviaux était laissée à la discrétion du compilateur. C'est-à-dire Le comportement du code dépend du compilateur et de sa version.

La seule utilisation autorisée des fonctions variables est d'interagir avec l'API C en code C ++. Pour tout le reste, y compris SFINAE , il existe des fonctions de modèle variables qui:

  • Connaître le nombre et les types de tous leurs arguments.
  • Tapez sûr, ne changez pas les types de leurs arguments.
  • Ils prennent en charge le passage d'arguments sous n'importe quelle forme - par valeur, par pointeur, par référence, par lien universel.
  • Comme toute autre fonction C ++, il n'y a aucune restriction sur les types d'arguments.
  • ( C ), .

Les fonctions de modèle variables peuvent être plus verbeuses par rapport à leurs homologues de style C et nécessitent parfois même leur propre version non modèle surchargée (traversée d'arguments récursifs). Ils sont plus difficiles à lire et à écrire. Mais tout cela est plus que payé par l'absence des lacunes énumérées et la présence des avantages énumérés.

Eh bien, la conclusion est simple: les fonctions variées dans le style C restent en C ++ uniquement en raison de la compatibilité descendante, et elles offrent un large éventail d'options pour tirer sur votre jambe. En C ++ moderne, il est fortement conseillé de ne pas en écrire de nouvelles et, si possible, de ne pas utiliser les fonctions C variables existantes. Les fonctions de modèles variables appartiennent au monde du C ++ moderne et sont beaucoup plus sécurisées. Utilisez-les.

Littérature et sources



PS


Il est facile de trouver et de télécharger des versions électroniques des livres mentionnés sur le net. Mais je ne suis pas sûr que ce sera légal, donc je ne donne pas de liens.

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


All Articles