Vous le voyez utilisé dans les instructions de boucle, mais c'est une syntaxe légale n'importe où. Quelles utilisations en avez-vous trouvées ailleurs, le cas échéant?
Le langage C (ainsi que C++) est historiquement un mélange de deux styles de programmation complètement différents, que l'on peut appeler "programmation d'instructions" et "programmation d'expression". Comme vous le savez, chaque langage de programmation procédurale prend normalement en charge des constructions fondamentales telles que séquençage et branchement (voir Programmation structurée ). Ces constructions fondamentales sont présentes dans les langages C/C++ sous deux formes: l'une pour la programmation d'instructions, l'autre pour la programmation d'expressions.
Par exemple, lorsque vous écrivez votre programme en termes d'instructions, vous pouvez utiliser une séquence d'instructions séparées par ;
. Lorsque vous souhaitez effectuer un branchement, vous utilisez des instructions if
. Vous pouvez également utiliser des cycles et d'autres types d'instructions de transfert de contrôle.
En programmation d'expression, les mêmes constructions sont également à votre disposition. C'est en fait là que ,
L'opérateur entre en jeu. Opérateur ,
rien d'autre qu'un séparateur d'expressions séquentielles en C, c'est-à-dire l'opérateur ,
dans la programmation d'expression joue le même rôle que ;
fait dans la programmation d'instructions. La ramification dans la programmation d'expression se fait par ?:
opérateur et, alternativement, par les propriétés d'évaluation de court-circuit de &&
et ||
les opérateurs. (La programmation d'expression n'a pas de cycles cependant. Et pour les remplacer par la récursivité, vous devez appliquer la programmation d'instructions.)
Par exemple, le code suivant
a = Rand();
++a;
b = Rand();
c = a + b / 2;
if (a < c - 5)
d = a;
else
d = b;
qui est un exemple de programmation d'instructions traditionnelle, peut être réécrit en termes de programmation d'expression comme
a = Rand(), ++a, b = Rand(), c = a + b / 2, a < c - 5 ? d = a : d = b;
ou comme
a = Rand(), ++a, b = Rand(), c = a + b / 2, d = a < c - 5 ? a : b;
ou
d = (a = Rand(), ++a, b = Rand(), c = a + b / 2, a < c - 5 ? a : b);
ou
a = Rand(), ++a, b = Rand(), c = a + b / 2, (a < c - 5 && (d = a, 1)) || (d = b);
Inutile de dire que dans la pratique, la programmation d'instructions produit généralement du code C/C++ beaucoup plus lisible, nous utilisons donc normalement la programmation d'expression en quantités très bien mesurées et restreintes. Mais dans de nombreux cas, cela est pratique. Et la frontière entre ce qui est acceptable et ce qui ne l'est pas est, dans une large mesure, une question de préférence personnelle et de capacité à reconnaître et à lire les idiomes établis.
En complément: la conception même du langage est évidemment adaptée aux énoncés. Les instructions peuvent appeler librement des expressions, mais les expressions ne peuvent pas appeler des instructions (à part appeler des fonctions prédéfinies). Cette situation est modifiée d'une manière plutôt intéressante dans le compilateur GCC, qui prend en charge ce que l'on appelle "expressions de déclaration" en tant qu'extension (symétrique des "déclarations d'expression" dans la norme C). Les "expressions d'instructions" permettent à l'utilisateur d'insérer directement du code basé sur des instructions dans des expressions, tout comme il peut insérer du code basé sur des expressions dans des instructions en standard C.
Autre remarque supplémentaire: en langage C++, la programmation basée sur les foncteurs joue un rôle important, qui peut être considéré comme une autre forme de "programmation d'expression". Selon les tendances actuelles de la conception C++, il peut être considéré comme préférable à la programmation d'instructions traditionnelle dans de nombreuses situations.
Je pense que la virgule de C n'est généralement pas un bon style à utiliser simplement parce qu'il est très très facile à manquer - soit par quelqu'un d'autre essayant de lire/comprendre/corriger votre code, soit par vous-même un mois plus tard. En dehors des déclarations de variables et pour les boucles, bien sûr, où il est idiomatique.
Vous pouvez l'utiliser, par exemple, pour regrouper plusieurs instructions dans un opérateur ternaire (? :), ala:
int x = some_bool ? printf("WTF"), 5 : fprintf(stderr, "No, really, WTF"), 117;
mais mes dieux, pourquoi?!? (Je l'ai vu utilisé de cette façon dans du vrai code, mais je n'y ai malheureusement pas accès pour l'afficher)
Deux fonctionnalités d'opérateur virgule tueuses en C++:
a) Lire à partir du flux jusqu'à ce qu'une chaîne spécifique soit rencontrée (aide à garder le code SEC):
while (cin >> str, str != "STOP") {
//process str
}
b) Écrivez du code complexe dans les initialiseurs de constructeur:
class X : public A {
X() : A( (global_function(), global_result) ) {};
};
Je l'ai vu utilisé dans des macros où la macro prétend être une fonction et veut retourner une valeur mais doit d'abord faire un autre travail. C'est toujours moche et ressemble souvent à un hack dangereux.
Exemple simplifié:
#define SomeMacro(A) ( DoWork(A), Permute(A) )
Ici B=SomeMacro(A)
"renvoie" le résultat de Permute (A) et l'assigne à "B".
La bibliothèque Boost Assignment est un bon exemple de surcharge de l'opérateur virgule d'une manière utile et lisible. Par exemple:
using namespace boost::assign;
vector<int> v;
v += 1,2,3,4,5,6,7,8,9;
J'ai dû utiliser une virgule pour déboguer les verrous mutex pour mettre un message avant le verrou commence à attendre.
Je ne pouvais pas mais le message de journal dans le corps du constructeur de verrouillage dérivé, j'ai donc dû le mettre dans les arguments du constructeur de la classe de base en utilisant: baseclass ((log ("message"), actual_arg)) dans la liste d'initialisation. Notez la parenthèse supplémentaire.
Voici un extrait des cours:
class NamedMutex : public boost::timed_mutex
{
public:
...
private:
std::string name_ ;
};
void log( NamedMutex & ref__ , std::string const& )
{
LOG( << " waits for " << ref__.name_ );
}
class NamedUniqueLock : public boost::unique_lock< NamedMutex >
{
public:
NamedUniqueLock::NamedUniqueLock(
NamedMutex & ref__ ,
std::string const& ,
size_t const& nbmilliseconds )
:
boost::unique_lock< NamedMutex >( ( log( ref__ , ) , ref__ ) ,
boost::get_system_time() + boost::posix_time::milliseconds( nbmilliseconds ) ),
ref_( ref__ ),
name_( )
{
}
....
};
De la norme C:
L'opérande gauche d'un opérateur virgule est évalué comme une expression vide; il y a un point de séquence après son évaluation. Ensuite, l'opérande droit est évalué; le résultat a son type et sa valeur. (Un opérateur virgule ne donne pas de valeur l.)) Si vous tentez de modifier le résultat d'un opérateur virgule ou d'y accéder après le point de séquence suivant, le comportement n'est pas défini.
En bref, il vous permet de spécifier plusieurs expressions où C n'en attend qu'une. Mais en pratique, il est principalement utilisé dans les boucles for.
Notez que:
int a, b, c;
n'est PAS l'opérateur virgule, c'est une liste de déclarants.
En dehors d'une boucle for, et même il y a peut avoir un arôme d'odeur de code, le seul endroit que j'ai vu comme une bonne utilisation pour l'opérateur virgule est dans le cadre d'une suppression:
delete p, p = 0;
La seule valeur par rapport à l'alternative est que vous ne pouvez accidentellement copier/coller que la moitié de cette opération si elle est sur deux lignes.
J'aime aussi parce que si vous le faites par habitude, vous n'oublierez jamais l'affectation zéro. (Bien sûr, pourquoi p n'est pas dans certains types de wrapper auto_ptr, smart_ptr, shared_ptr, etc. est une question différente.)
Vous pouvez le surcharger (tant que cette question a une balise "C++"). J'ai vu du code, où une virgule surchargée a été utilisée pour générer des matrices. Ou des vecteurs, je ne me souviens pas exactement. N'est-ce pas joli (bien qu'un peu déroutant):
MyVector foo = 2, 3, 4, 5, 6;
Il est parfois utilisé dans les macros, telles que les macros de débogage comme ceci:
#define malloc(size) (printf("malloc(%d)\n", (int)(size)), malloc((size)))
(Mais regardez cet horrible échec , par vous-même vraiment, pour ce qui peut arriver quand vous en faites trop.)
Mais à moins que vous n'en ayez vraiment besoin ou que vous soyez sûr qu'il rend le code plus lisible et plus facile à gérer, je vous déconseille d'utiliser l'opérateur virgule.
Étant donné la citation de @Nicolas Goy de la norme, il semble que vous pourriez écrire une ligne pour des boucles comme:
int a, b, c;
for(a = 0, b = 10; c += 2*a+b, a <= b; a++, b--);
printf("%d", c);
Mais bon Dieu, mec, voulez-vous vraiment rendre votre code C plus obscur de cette façon?
Il est très utile pour ajouter des commentaires dans les macros ASSERT
:
ASSERT(("This value must be true.", x));
Étant donné que la plupart des macros de style d'assertion afficheront l'intégralité du texte de leur argument, cela ajoute un peu d'informations supplémentaires à l'assertion.
En général, j'évite d'utiliser l'opérateur virgule car cela rend le code moins lisible. Dans presque tous les cas, il serait plus simple et plus clair de ne faire que deux déclarations. Comme:
foo=bar*2, plugh=hoo+7;
n'offre aucun avantage clair sur:
foo=bar*2;
plugh=hoo+7;
Le seul endroit en plus des boucles où je l'ai utilisé dans les constructions if/else, comme:
if (a==1)
... do something ...
else if (function_with_side_effects_including_setting_b(), b==2)
... do something that relies on the side effects ...
Vous pouvez placer la fonction avant l'IF, mais si la fonction prend beaucoup de temps à s'exécuter, vous voudrez peut-être éviter de la faire si ce n'est pas nécessaire, et si la fonction ne doit pas être effectuée sauf si a! = 1, alors ce n'est pas un option. L'alternative consiste à imbriquer les IF d'une couche supplémentaire. C'est en fait ce que je fais habituellement parce que le code ci-dessus est un peu cryptique. Mais je l'ai fait de temps en temps par virgule, car l'imbrication est également cryptique.
Pour moi, le seul cas vraiment utile avec des virgules en C est de les utiliser pour effectuer quelque chose de manière conditionnelle.
if (something) dothis(), dothat(), x++;
cela équivaut à
if (something) { dothis(); dothat(); x++; }
Il ne s'agit pas de "taper moins", c'est parfois très clair.
Les boucles sont aussi comme ça:
while(true) x++, y += 5;
Bien sûr, les deux ne peuvent être utiles que lorsque la partie conditionnelle ou la partie exécutable de la boucle est assez petite, deux à trois opérations.
Je l'utilise souvent pour exécuter une fonction d'initialisation statique dans certains fichiers cpp, pour éviter les problèmes d'initialisation paresseuse avec les singletons classiques:
void* s_static_pointer = 0;
void init() {
configureLib();
s_static_pointer = calculateFancyStuff(x,y,z);
regptr(s_static_pointer);
}
bool s_init = init(), true; // just run init() before anything else
Foo::Foo() {
s_static_pointer->doStuff(); // works properly
}
qemu a du code qui utilise l'opérateur virgule dans la partie conditionnelle d'une boucle for (voir QTAILQ_FOREACH_SAFE dans qemu-queue.h). Ce qu'ils ont fait se résume à ce qui suit:
#include <stdio.h>
int main( int argc, char* argv[] ){
int x = 0, y = 0;
for( x = 0; x < 3 && (y = x+1,1); x = y ){
printf( "%d, %d\n", x, y );
}
printf( "\n%d, %d\n\n", x, y );
for( x = 0, y = x+1; x < 3; x = y, y = x+1 ){
printf( "%d, %d\n", x, y );
}
printf( "\n%d, %d\n", x, y );
return 0;
}
... avec la sortie suivante:
0, 1
1, 2
2, 3
3, 3
0, 1
1, 2
2, 3
3, 4
La première version de cette boucle a les effets suivants:
&&
, l'affectation n'est pas évaluée après la dernière itérationTrouvé dans l'initialisation du tableau:
Lorsque j'initialise un tableau a[][]
:
int a[2][5]={(8,9,7,67,11),(7,8,9,199,89)};
puis afficher les éléments du tableau.
Je reçois:
11 89 0 0 0
0 0 0 0 0
La seule fois où j'ai vu le ,
L'opérateur utilisé en dehors d'une boucle for
devait effectuer une évaluation dans une instruction ternaire. C'était il y a longtemps donc je ne peux pas me souvenir de la déclaration exacte mais c'était quelque chose comme:
int ans = isRunning() ? total += 10, newAnswer(total) : 0;
De toute évidence, aucune personne sensée n'écrirait du code comme celui-ci, mais l'auteur était un génie diabolique qui construisait des instructions c en fonction du code assembleur qu'elles ont généré, et non de la lisibilité. Par exemple, il utilisait parfois des boucles au lieu des instructions if car il préférait l'assembleur qu'il générait.
Son code était très rapide mais impossible à maintenir, je suis content de ne plus avoir à travailler avec.
Je l'ai utilisé pour une macro pour "affecter une valeur de n'importe quel type à un tampon de sortie pointé par un caractère *, puis incrémenter le pointeur du nombre d'octets requis", comme ceci:
#define ASSIGN_INCR(p, val, type) ((*((type) *)(p) = (val)), (p) += sizeof(type))
L'utilisation de l'opérateur virgule signifie que la macro peut être utilisée dans des expressions ou des instructions comme vous le souhaitez:
if (need_to_output_short)
ASSIGN_INCR(ptr, short_value, short);
latest_pos = ASSIGN_INCR(ptr, int_value, int);
send_buff(outbuff, (int)(ASSIGN_INCR(ptr, last_value, int) - outbuff));
Cela a réduit la frappe répétitive mais vous devez faire attention à ce que cela ne devienne pas trop illisible.
Veuillez consulter ma version trop longue de cette réponse ici .
Il peut être utile pour le "code golf":
Le ,
Dans if(i>0)t=i,i=0;
enregistre deux caractères.