170 votes

Une alternative standard à l'astuce ##__VA_ARGS__ de GCC ?

Il existe un bien connu problème avec des arguments vides pour les macros variadiques en C99.

exemple :

#define FOO(...)       printf(__VA_ARGS__)
#define BAR(fmt, ...)  printf(fmt, __VA_ARGS__)

FOO("this works fine");
BAR("this breaks!");

L'utilisation de BAR() ci-dessus est en effet incorrect selon la norme C99, puisqu'il se développera en :

printf("this breaks!",);

Notez la virgule de fin de phrase - ce n'est pas possible.

Certains compilateurs (par exemple, Visual Studio 2010) se débarrasseront discrètement de cette virgule de fin pour vous. D'autres compilateurs (par exemple, GCC) prennent en charge la mise en place de la virgule de fin. ## devant __VA_ARGS__ comme ça :

#define BAR(fmt, ...)  printf(fmt, ##__VA_ARGS__)

Mais existe-t-il un moyen conforme aux normes d'obtenir ce comportement ? Peut-être en utilisant plusieurs macros ?

En ce moment, le ## semble assez bien supportée (du moins sur mes plateformes), mais je préférerais vraiment utiliser une solution conforme aux normes.

Préventif : Je sais que je pourrais simplement écrire une petite fonction. J'essaie de le faire en utilisant des macros.

Editar : Voici un exemple (bien que simple) de la raison pour laquelle je voudrais utiliser BAR() :

#define BAR(fmt, ...)  printf(fmt "\n", ##__VA_ARGS__)

BAR("here is a log message");
BAR("here is a log message with a param: %d", 42);

Cela ajoute automatiquement une nouvelle ligne à mes instructions de journalisation BAR(), en supposant que fmt est toujours une chaîne de caractères C doublement citée. Il n'imprime PAS la nouvelle ligne comme un printf() séparé, ce qui est avantageux si l'enregistrement est tamponné par ligne et provient de plusieurs sources de manière asynchrone.

3 votes

Pourquoi utiliser BAR au lieu de FOO en premier lieu ?

0 votes

@GMan : J'ai ajouté un exemple à la fin.

0 votes

Dans ce cas, il suffit d'utiliser une macro multi-états avec une balise printf("\n"); à la fin.

122voto

Richard Hansen Points 13044

Il existe une astuce pour compter les arguments que vous pouvez utiliser.

Voici une façon conforme à la norme de mettre en œuvre la deuxième méthode. BAR() exemple dans la question de jwd :

#include <stdio.h>

#define BAR(...) printf(FIRST(__VA_ARGS__) "\n" REST(__VA_ARGS__))

/* expands to the first argument */
#define FIRST(...) FIRST_HELPER(__VA_ARGS__, throwaway)
#define FIRST_HELPER(first, ...) first

/*
 * if there's only one argument, expands to nothing.  if there is more
 * than one argument, expands to a comma followed by everything but
 * the first argument.  only supports up to 9 arguments but can be
 * trivially expanded.
 */
#define REST(...) REST_HELPER(NUM(__VA_ARGS__), __VA_ARGS__)
#define REST_HELPER(qty, ...) REST_HELPER2(qty, __VA_ARGS__)
#define REST_HELPER2(qty, ...) REST_HELPER_##qty(__VA_ARGS__)
#define REST_HELPER_ONE(first)
#define REST_HELPER_TWOORMORE(first, ...) , __VA_ARGS__
#define NUM(...) \
    SELECT_10TH(__VA_ARGS__, TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE,\
                TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE, ONE, throwaway)
#define SELECT_10TH(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, ...) a10

int
main(int argc, char *argv[])
{
    BAR("first test");
    BAR("second test: %s", "a string");
    return 0;
}

Cette même astuce est utilisée pour :

Explication

La stratégie consiste à séparer __VA_ARGS__ dans le premier argument et le reste (s'il y en a). Cela permet d'insérer des choses après le premier argument mais avant le second (s'il est présent).

FIRST()

Cette macro s'étend simplement au premier argument, en ignorant le reste.

La mise en œuvre est simple. Le site throwaway garantit que l'argument FIRST_HELPER() reçoit deux arguments, ce qui est nécessaire car le ... doit en avoir au moins un. Avec un argument, il se développe comme suit :

  1. FIRST(firstarg)
  2. FIRST_HELPER(firstarg, throwaway)
  3. firstarg

Avec deux ou plus, il se développe comme suit :

  1. FIRST(firstarg, secondarg, thirdarg)
  2. FIRST_HELPER(firstarg, secondarg, thirdarg, throwaway)
  3. firstarg

REST()

Cette macro s'étend à tout sauf au premier argument (y compris la virgule après le premier argument, s'il y a plus d'un argument).

L'implémentation de cette macro est beaucoup plus compliquée. La stratégie générale consiste à compter le nombre d'arguments (un ou plus d'un), puis à étendre à soit REST_HELPER_ONE() (si un seul argument est donné) ou REST_HELPER_TWOORMORE() (si deux ou plusieurs arguments sont donnés). REST_HELPER_ONE() s'étend simplement à rien -- il n'y a pas d'arguments après le premier, donc les arguments restants sont l'ensemble vide. REST_HELPER_TWOORMORE() est également simple -- elle se développe en une virgule suivie de tout sauf du premier argument.

Les arguments sont comptés en utilisant le NUM() macro. Cette macro se développe en ONE si un seul argument est donné, TWOORMORE si entre deux et neuf arguments sont donnés, et se casse si 10 arguments ou plus sont donnés (parce qu'il s'étend au 10ème argument).

El NUM() utilise la macro SELECT_10TH() pour déterminer le nombre d'arguments. Comme son nom l'indique, SELECT_10TH() s'étend simplement jusqu'à son 10ème argument. A cause de l'ellipse, SELECT_10TH() doit recevoir au moins 11 arguments (la norme indique qu'il doit y avoir au moins un argument pour l'ellipse). C'est pourquoi NUM() passe throwaway comme dernier argument (sans cela, passer un argument à NUM() aurait pour résultat que seulement 10 arguments seraient passés à SELECT_10TH() ce qui serait contraire à la norme).

Sélection de l'un ou l'autre REST_HELPER_ONE() o REST_HELPER_TWOORMORE() se fait en concaténant REST_HELPER_ avec l'expansion de NUM(__VA_ARGS__) en REST_HELPER2() . Notez que le but de REST_HELPER() est de s'assurer que NUM(__VA_ARGS__) est entièrement développée avant d'être concaténée avec REST_HELPER_ .

L'expansion avec un argument est la suivante :

  1. REST(firstarg)
  2. REST_HELPER(NUM(firstarg), firstarg)
  3. REST_HELPER2(SELECT_10TH(firstarg, TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE, ONE, throwaway), firstarg)
  4. REST_HELPER2(ONE, firstarg)
  5. REST_HELPER_ONE(firstarg)
  6. (vide)

L'expansion avec deux arguments ou plus se déroule comme suit :

  1. REST(firstarg, secondarg, thirdarg)
  2. REST_HELPER(NUM(firstarg, secondarg, thirdarg), firstarg, secondarg, thirdarg)
  3. REST_HELPER2(SELECT_10TH(firstarg, secondarg, thirdarg, TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE, ONE, throwaway), firstarg, secondarg, thirdarg)
  4. REST_HELPER2(TWOORMORE, firstarg, secondarg, thirdarg)
  5. REST_HELPER_TWOORMORE(firstarg, secondarg, thirdarg)
  6. , secondarg, thirdarg

1 votes

Notez que cela échouera si vous appelez BAR avec 10 arguments ou plus, et bien qu'il soit relativement facile d'étendre à plus d'arguments, il aura toujours une limite supérieure sur le nombre d'arguments qu'il peut gérer.

2 votes

@ChrisDodd : Correct. Malheureusement, il ne semble pas y avoir de moyen d'éviter une limite du nombre d'arguments sans s'appuyer sur des extensions spécifiques au compilateur. De même, je ne connais pas de moyen de tester de manière fiable s'il y a trop d'arguments (de sorte qu'un message d'erreur de compilateur utile puisse être imprimé, plutôt qu'un échec étrange).

71voto

Zack Points 44583

Il est possible d'éviter l'utilisation de l'outil GCC ,##__VA_ARGS__ si vous êtes prêt à accepter une limite supérieure codée en dur sur le nombre d'arguments que vous pouvez passer à votre macro variadique, comme décrit dans le document La réponse de Richard Hansen à cette question . Si vous ne voulez pas avoir une telle limite, cependant, à ma connaissance, ce n'est pas possible en utilisant seulement les caractéristiques du préprocesseur spécifiées par C99 ; vous devez utiliser une extension du langage. clang et icc ont adopté cette extension GCC, mais MSVC ne l'a pas fait.

En 2001, j'ai rédigé l'extension GCC pour la normalisation (et l'extension connexe qui vous permet d'utiliser un nom autre que __VA_ARGS__ pour le paramètre de repos) dans document N976 mais elle n'a reçu aucune réponse de la part de la commission ; je ne sais même pas si quelqu'un l'a lue. En 2016, il a été proposé à nouveau en N2023 et j'encourage tous ceux qui savent comment se déroule cette proposition à nous le faire savoir dans les commentaires.

2 votes

À en juger par mon incapacité à trouver une solution sur le web et le manque de réponses ici, je suppose que vous avez raison ;) :

2 votes

Est n976 à quoi vous faites référence ? J'ai cherché dans le reste du Groupe de travail C 's documents pour une réponse mais je n'en ai jamais trouvé. Ce n'était même pas dans le l'ordre ordre ordre du jour de l'ordre du jour de l'ordre du jour de l'ordre du jour de l'ordre du jour de l'ordre du jour suivant . Le seul autre résultat sur ce sujet était le commentaire n°4 de Norvège dans n868 de retour d'avant la ratification de C99 (encore une fois sans discussion de suivi).

4 votes

Oui, plus précisément la seconde moitié de cette phrase. Il y a peut-être eu une discussion sur comp.std.c mais je n'ai pas pu en trouver dans Google Groups à l'instant ; il n'a certainement jamais reçu l'attention du comité actuel (ou si c'est le cas, personne ne m'en a jamais parlé).

19voto

Marsh Ray Points 2012

Ce n'est pas une solution générale, mais dans le cas de printf, vous pourriez ajouter une nouvelle ligne comme suit :

#define BAR_HELPER(fmt, ...) printf(fmt "\n%s", __VA_ARGS__)
#define BAR(...) BAR_HELPER(__VA_ARGS__, "")

Je crois qu'il ignore tous les arguments supplémentaires qui ne sont pas référencés dans la chaîne de format. Vous pourriez donc probablement vous en sortir avec :

#define BAR_HELPER(fmt, ...) printf(fmt "\n", __VA_ARGS__)
#define BAR(...) BAR_HELPER(__VA_ARGS__, 0)

Je ne peux pas croire que C99 ait été approuvé sans une manière standard de faire cela. AFAICT le problème existe aussi dans C++11.

0 votes

Le problème avec ce 0 supplémentaire est qu'il se retrouvera dans le code s'il appelle la fonction vararg. Consultez la solution fournie par Richard Hansen

1 votes

@Pavel a raison pour le deuxième exemple, mais le premier fonctionne très bien. +1.

11voto

DRayX Points 451

Il existe un moyen de gérer ce cas spécifique en utilisant quelque chose comme Préprocesseur Boost . Vous pouvez utiliser BOOST_PP_VARIADIC_SIZE pour vérifier la taille de la liste d'arguments, et ensuite étendre conditionnellement à une autre macro. Le seul inconvénient de cette méthode est qu'elle ne peut pas faire la distinction entre les arguments 0 et 1, et la raison en est claire si l'on considère ce qui suit :

BOOST_PP_VARIADIC_SIZE()      // expands to 1
BOOST_PP_VARIADIC_SIZE(,)     // expands to 2
BOOST_PP_VARIADIC_SIZE(,,)    // expands to 3
BOOST_PP_VARIADIC_SIZE(a)     // expands to 1
BOOST_PP_VARIADIC_SIZE(a,)    // expands to 2
BOOST_PP_VARIADIC_SIZE(,b)    // expands to 2
BOOST_PP_VARIADIC_SIZE(a,b)   // expands to 2
BOOST_PP_VARIADIC_SIZE(a, ,c) // expands to 3

La liste d'arguments de la macro vide consiste en fait en un argument qui se trouve être vide.

Dans ce cas, nous avons de la chance puisque la macro que vous souhaitez a toujours au moins un argument, nous pouvons l'implémenter comme deux macros "surcharge" :

#define BAR_0(fmt) printf(fmt "\n")
#define BAR_1(fmt, ...) printf(fmt "\n", __VA_ARGS__)

Et ensuite, une autre macro pour passer de l'une à l'autre, par exemple :

#define BAR(...) \
    BOOST_PP_CAT(BAR_, BOOST_PP_GREATER(
        BOOST_PP_VARIADIC_SIZE(__VA_ARGS__), 1))(__VA_ARGS__) \
    /**/

ou

#define BAR(...) BOOST_PP_IIF( \
    BOOST_PP_GREATER(BOOST_PP_VARIADIC_SIZE(__VA_ARGS__), 1), \
        BAR_1, BAR_0)(__VA_ARGS__) \
    /**/

Celui que vous trouvez le plus lisible (je préfère le premier car il vous donne une forme générale pour surcharger les macros sur le nombre d'arguments).

Il est également possible de faire cela avec une seule macro en accédant à la liste des arguments de la variable et en la modifiant, mais c'est beaucoup moins lisible, et c'est très spécifique à ce problème :

#define BAR(...) printf( \
    BOOST_PP_VARIADIC_ELEM(0, __VA_ARGS__) "\n" \
    BOOST_PP_COMMA_IF( \
        BOOST_PP_GREATER(BOOST_PP_VARIADIC_SIZE(__VA_ARGS__), 1)) \
    BOOST_PP_ARRAY_ENUM(BOOST_PP_ARRAY_POP_FRONT( \
        BOOST_PP_VARIADIC_TO_ARRAY(__VA_ARGS__)))) \
    /**/

De plus, pourquoi n'y a-t-il pas de BOOST_PP_ARRAY_ENUM_TRAILING ? Cela rendrait cette solution beaucoup moins horrible.

Edit : Très bien, voici un BOOST_PP_ARRAY_ENUM_TRAILING, et une version qui l'utilise (c'est maintenant ma solution préférée) :

#define BOOST_PP_ARRAY_ENUM_TRAILING(array) \
    BOOST_PP_COMMA_IF(BOOST_PP_ARRAY_SIZE(array)) BOOST_PP_ARRAY_ENUM(array) \
    /**/

#define BAR(...) printf( \
    BOOST_PP_VARIADIC_ELEM(0, __VA_ARGS__) "\n" \
    BOOST_PP_ARRAY_ENUM_TRAILING(BOOST_PP_ARRAY_POP_FRONT( \
        BOOST_PP_VARIADIC_TO_ARRAY(__VA_ARGS__)))) \
    /**/

1 votes

Sympa d'apprendre sur Boost.Preprocessor, +1. Notez que BOOST_PP_VARIADIC_SIZE() utilise le même truc de comptage d'arguments que j'ai documenté dans ma réponse, et a la même limitation (il se cassera si vous passez plus d'un certain nombre d'arguments).

1 votes

Yep, j'ai vu que ton approche était la même que celle utilisée par Boost, mais la solution boost est très bien maintenue, et a beaucoup d'autres fonctionnalités vraiment utiles à utiliser lors du développement de macros plus sophistiquées. Le truc de récursion est particulièrement cool (et utilisé en coulisse dans la dernière approche qui utilise BOOST_PP_ARRAY_ENUM).

1 votes

Une réponse de Boost qui s'applique réellement à la c tag ! Hourra !

5voto

User123abc Points 192

J'ai rencontré un problème similaire récemment, et je pense qu'il existe une solution.

L'idée clé est qu'il existe un moyen d'écrire une macro NUM_ARGS pour compter le nombre d'arguments donnés à une macro variadique. Vous pouvez utiliser une variation de NUM_ARGS pour construire NUM_ARGS_CEILING2 qui peut vous dire si une macro variadique reçoit 1 argument ou 2 arguments ou plus. Vous pouvez alors écrire votre Bar de sorte qu'elle utilise NUM_ARGS_CEILING2 y CONCAT pour envoyer ses arguments à l'une des deux macros d'aide : une qui attend exactement 1 argument, et une autre qui attend un nombre variable d'arguments supérieur à 1.

Voici un exemple où j'utilise cette astuce pour écrire la macro UNIMPLEMENTED qui est très similaire à BAR :

ÉTAPE 1 :

/** 
 * A variadic macro which counts the number of arguments which it is
 * passed. Or, more precisely, it counts the number of commas which it is
 * passed, plus one.
 *
 * Danger: It can't count higher than 20. If it's given 0 arguments, then it
 * will evaluate to 1, rather than to 0.
 */

#define NUM_ARGS(...)                                                   \
    NUM_ARGS_COUNTER(__VA_ARGS__, 20, 19, 18, 17, 16, 15, 14, 13,       \
                     12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1)    

#define NUM_ARGS_COUNTER(a1, a2, a3, a4, a5, a6, a7,        \
                         a8, a9, a10, a11, a12, a13,        \
                         a14, a15, a16, a17, a18, a19, a20, \
                         N, ...)                            \
    N

ÉTAPE 1.5 :

/*
 * A variant of NUM_ARGS that evaluates to 1 if given 1 or 0 args, or
 * evaluates to 2 if given more than 1 arg. Behavior is nasty and undefined if
 * it's given more than 20 args.
 */

#define NUM_ARGS_CEIL2(...)                                           \
    NUM_ARGS_COUNTER(__VA_ARGS__, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, \
                     2, 2, 2, 2, 2, 2, 2, 1)

Étape 2 :

#define _UNIMPLEMENTED1(msg)                                        \
    log("My creator has forsaken me. %s:%s:%d." msg, __FILE__,      \
        __func__, __LINE__)

#define _UNIMPLEMENTED2(msg, ...)                                   \
    log("My creator has forsaken me. %s:%s:%d." msg, __FILE__,      \
        __func__, __LINE__, __VA_ARGS__)

ÉTAPE 3 :

#define UNIMPLEMENTED(...)                                              \
    CONCAT(_UNIMPLEMENTED, NUM_ARGS_CEIL2(__VA_ARGS__))(__VA_ARGS__)

Où CONCAT est implémenté de la manière habituelle. Une petite astuce, si ce qui précède vous semble confus : le but de CONCAT est d'étendre à une autre macro "call".

Notez que NUM_ARGS lui-même n'est pas utilisé. Je l'ai juste inclus pour illustrer l'astuce de base ici. Voir Le blog P99 de Jens Gustedt pour un bon traitement de la question.

Deux notes :

  • NUM_ARGS est limité dans le nombre d'arguments qu'il traite. Le mien ne peut en gérer que 20, bien que ce nombre soit totalement arbitraire.

  • NUM_ARGS, comme indiqué, a un piège dans la mesure où il renvoie 1 lorsqu'on lui donne 0 argument. L'essentiel est que NUM_ARGS compte techniquement [virgules + 1], et non les arguments. Dans ce cas particulier cas particulier, cela fonctionne en fait à notre avantage. _UNIMPLEMENTED1 gère très bien un token vide. et cela nous évite d'avoir à écrire _UNIMPLEMENTED0. Gustedt a une Gustedt a également une solution de contournement pour cela, bien que je ne l'aie pas utilisée et que je ne sois pas sûr qu'elle fonctionnerait pour ce que nous faisons ici.

0 votes

+1 pour avoir mis en avant le truc du comptage d'arguments, -1 pour être vraiment difficile à suivre.

0 votes

Les commentaires que vous avez ajoutés constituent une amélioration, mais il reste un certain nombre de problèmes : 1. Vous discutez et définissez NUM_ARGS mais ne l'utilisez pas. 2. Quel est le but de UNIMPLEMENTED ? 3. Vous ne résolvez jamais le problème de l'exemple dans la question. 4. Parcourir l'expansion étape par étape permettrait d'illustrer son fonctionnement et d'expliquer le rôle de chaque macro d'aide. 5. Discuter de l'absence d'arguments est une distraction ; le PO demandait la conformité aux normes, et l'absence d'arguments est interdite (C99 6.10.3p4). 6. Étape 1.5 ? Pourquoi pas l'étape 2 ? 7. "Steps" implique des actions qui se produisent séquentiellement ; ceci est juste du code.

0 votes

8. Vous faites un lien vers l'ensemble du blog, et non vers l'article concerné. Je n'ai pas trouvé l'article auquel vous faites référence. 9. Le dernier paragraphe est maladroit : Cette méthode es obscure ; c'est pourquoi personne d'autre n'avait posté une solution correcte auparavant. De plus, si elle fonctionne et respecte la norme, la réponse de Zack doit être fausse. 10. Vous devriez définir CONCAT() -- ne supposez pas que les lecteurs savent comment ça marche.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X