C ++, déclaration de variable dans l'expression 'if'

114

Que se passe t-il ici?

if(int a = Func1())
{
    // Works.
}

if((int a = Func1()))
{
    // Fails to compile.
}

if((int a = Func1())
    && (int b = Func2()))
)
{
    // Do stuff with a and b.
    // This is what I'd really like to be able to do.
}

La section 6.4.3 de la norme 2003 explique comment les variables déclarées dans une condition d'énoncé de sélection ont une portée qui s'étend jusqu'à la fin des sous-instructions contrôlées par la condition. Mais je ne vois pas où cela dit quoi que ce soit sur le fait de ne pas pouvoir mettre de parenthèses autour de la déclaration, ni sur une seule déclaration par condition.

Cette limitation est ennuyeuse même dans les cas où une seule déclaration dans la condition est requise. Considère ceci.

bool a = false, b = true;

if(bool x = a || b)
{

}

Si je veux entrer la portée 'if' -body avec x défini sur false, la déclaration a besoin de parenthèses (puisque l'opérateur d'affectation a une priorité inférieure à l'OR logique), mais comme la parenthèse ne peut pas être utilisée, elle nécessite la déclaration de x à l'extérieur le corps, la fuite de cette déclaration à une portée plus grande que ce qui est souhaité. Évidemment, cet exemple est trivial mais un cas plus réaliste serait celui où a et b sont des fonctions renvoyant des valeurs qui doivent être testées

Est-ce que ce que je veux faire est non conforme à la norme, ou est-ce que mon compilateur ne fait que me casser les couilles (VS2008)?

Neutrino
la source
6
"Si je veux entrer dans la boucle avec" <- vos exemples ont if. ifn'est pas une boucle, c'est un conditionnel.
crashmstr
2
@crashmstr: true, mais les conditions pour while sont les mêmes que pour if.
Mike Seymour
2
Cela ne peut-il pas être fait avec l'opérateur virgule? Je veux dire if (int a = foo(), int b = bar(), a && b):? Si l'opérateur virgule n'est pas surchargé, la norme dit que les expressions sont évaluées de gauche à droite et la valeur du résultat est la dernière expression. Cela fonctionne avec l' forinitialisation des boucles, pourquoi pas ici?
Archie
@Archie: J'ai juste essayé ça, je n'ai pas pu le faire fonctionner. Peut-être pouvez-vous donner un exemple concret?
James Johnston
@JamesJohnston: J'ai juste essayé aussi, et cela ne semble pas fonctionner. Cette idée vient du haut de ma tête, j'ai été suggéré par la façon dont iffonctionne, et cela semble être une mauvaise hypothèse.
Archie

Réponses:

63

Depuis C ++ 17, ce que vous essayez de faire est enfin possible :

if (int a = Func1(), b = Func2(); a && b)
{
    // Do stuff with a and b.
}

Notez l'utilisation de ;au lieu de ,pour séparer la déclaration et la condition réelle.

fwyzard
la source
23
Agréable! J'ai toujours pensé que j'étais en avance sur mon temps.
Neutrino
106

Je pense que vous avez déjà fait allusion à la question. Que doit faire le compilateur avec ce code?

if (!((1 == 0) && (bool a = false))) {
    // what is "a" initialized to?

L'opérateur "&&" est un ET logique de court-circuit. Cela signifie que si la première partie (1==0)s'avère fausse, la deuxième partie (bool a = false)ne doit pas être évaluée car on sait déjà que la réponse finale sera fausse. Si ce (bool a = false)n'est pas évalué, que faire plus tard avec le code qui utilise a? Ne devrions-nous tout simplement pas initialiser la variable et la laisser non définie? Pourrions-nous l'initialiser à la valeur par défaut? Et si le type de données était une classe et que cela avait des effets secondaires indésirables? Et si au lieu de boolvous utilisiez une classe et qu'elle n'avait pas de constructeur par défaut tel que l'utilisateur doit fournir des paramètres - que faisons-nous alors?

Voici un autre exemple:

class Test {
public:
    // note that no default constructor is provided and user MUST
    // provide some value for parameter "p"
    Test(int p);
}

if (!((1 == 0) && (Test a = Test(5)))) {
    // now what do we do?!  what is "a" set to?

Il semble que la limitation que vous avez trouvée semble parfaitement raisonnable - elle empêche ce genre d'ambiguïtés de se produire.

James Johnston
la source
1
Bon point. Vous voudrez peut-être mentionner explicitement le court-circuit, au cas où l'OP ou d'autres ne le seraient pas familiers.
Chris Cooper
7
Je n'avais pas pensé à ça. Bien que dans l'exemple que vous avez fourni, le court-circuit empêche l'entrée de la portée de l'instruction conditionnelle, auquel cas la partie de l'expression qui déclare la variable non traitée n'est pas un problème, car sa portée est limitée à celle de l'instruction conditionnelle. Dans ce cas, ne serait-il pas préférable que le compilateur lève une erreur uniquement dans les cas où il est possible que la portée de l'instruction conditionnelle soit entrée lorsqu'une partie de l'expression qui a déclaré une variable n'a pas été traitée? Ce qui n'était pas le cas dans les exemples que j'ai donnés.
Neutrino
@Neutrino À première vue, votre idée ressemble un peu au problème SAT, ce qui n'est pas si simple à résoudre, du moins dans le cas général.
Christian Rau
5
Ce que vous expliquez à propos de tous les problèmes liés au fait d'avoir plusieurs déclarations de variables dans la condition if et le fait que vous ne pouvez les utiliser que de manière restreinte me fait me demander pourquoi diable ce type de déclaration a été introduit en premier lieu. Je n'avais jamais ressenti le besoin d'une telle syntaxe avant de la voir dans un exemple de code. Je trouve cette syntaxe plutôt maladroite et je pense que déclarer la variable avant le bloc if est beaucoup plus lisible. Si vous avez vraiment besoin de restreindre la portée de cette variable, vous pouvez mettre un bloc supplémentaire autour du bloc if. Je n'ai jamais utilisé cette syntaxe.
Giorgio
2
Personnellement, je pense qu'il serait plutôt élégant de pouvoir restreindre la portée des variables que vous utilisez à exactement la portée du bloc d'instructions qui doit les utiliser, sans avoir à recourir à des mesures laides comme des accolades de portée supplémentaires imbriquées.
Neutrino
96

La condition dans une instruction ifor whilepeut être soit une expression , soit une déclaration de variable unique (avec initialisation).

Vos deuxième et troisième exemples ne sont ni des expressions valides, ni des déclarations valides, car une déclaration ne peut pas faire partie d'une expression. S'il serait utile de pouvoir écrire du code comme votre troisième exemple, cela nécessiterait une modification significative de la syntaxe du langage.

Je ne vois pas où cela dit quoi que ce soit sur l'impossibilité de mettre entre parenthèses la déclaration, ni sur une seule déclaration par condition.

La spécification de syntaxe en 6.4 / 1 donne ce qui suit pour la condition:

condition:
    expression
    type-specifier-seq declarator = assignment-expression

spécifiant une seule déclaration, sans parenthèses ni autres ornements.

Mike Seymour
la source
3
Y a-t-il une raison ou un contexte à cela?
Tomáš Zato - Réintégrer Monica
23

Si vous souhaitez inclure des variables dans une portée plus étroite, vous pouvez toujours utiliser des { }

//just use { and }
{
    bool a = false, b = true;

    if(bool x = a || b)
    {
        //...
    }
}//a and b are out of scope
crashmstr
la source
5
+1. De plus, je déplacerais la déclaration de x vers le bloc environnant: pourquoi devrait-il avoir un statut spécial pour a et b?
Giorgio
1
Évident, mais pas convaincant: la même chose pourrait être dite pour les variables de boucle ordinaires. (Certes, le besoin d'une portée variable limitée est beaucoup plus courant dans les boucles.)
Peter - Réintégrer Monica
18

La dernière section fonctionne déjà, il vous suffit de l'écrire légèrement différemment:

if (int a = Func1())
{
   if (int b = Func2())
   {
        // do stuff with a and b
   }
}
Bo Persson
la source
2

Voici une solution de contournement laide utilisant une boucle (si les deux variables sont des entiers):

#include <iostream>

int func1()
{
    return 4;
}

int func2()
{
    return 23;
}

int main()
{
    for (int a = func1(), b = func2(), i = 0;
        i == 0 && a && b; i++)
    {
        std::cout << "a = " << a << std::endl;
        std::cout << "b = " << b << std::endl;
    }

    return 0;
}

Mais cela déroutera les autres programmeurs et c'est un code plutôt mauvais, donc déconseillé.

Un simple {}bloc englobant (comme déjà recommandé) est beaucoup plus facile à lire:

{
    int a = func1();
    int b = func2();

    if (a && b)
    {
        std::cout << "a = " << a << std::endl;
        std::cout << "b = " << b << std::endl;
    }
}
basique6
la source
1

Une chose à noter est également que les expressions à l'intérieur du plus grand bloc if

if (!((1 == 0) && (bool a = false)))

ne sont pas nécessairement garantis d'être évalués de gauche à droite. Un bogue assez subtil que j'ai eu à l'époque était lié au fait que le compilateur testait en fait de droite à gauche au lieu de gauche à droite.

DukeBrymin
la source
5
De nos jours, cependant, C99 exige que && et || sont évalués de gauche à droite.
b0fh le
2
Je pense qu'une évaluation de droite à gauche des arguments n'a jamais été possible en raison de la logique de court-circuit. Il était toujours utilisé pour des choses comme un test de pointeur et de pointe dans une seule expression, comme if(p && p->str && *p->str) .... La droite-gauche aurait été mortelle et non subtile. Avec d'autres opérateurs - même affectation! - et les arguments d'appel de fonction vous avez raison, mais pas avec les opérateurs de court-circuit.
Peter - Réintègre Monica
1

Avec un peu de magie de modèle, vous pouvez en quelque sorte contourner le problème de ne pas pouvoir déclarer plusieurs variables:

#include <stdio.h>

template <class LHS, class RHS>
struct And_t {
  LHS lhs;
  RHS rhs;

  operator bool () {
    bool b_lhs(lhs);
    bool b_rhs(rhs);
    return b_lhs && b_rhs;
  }
};
template <class LHS, class RHS> 
And_t<LHS, RHS> And(const LHS& lhs, const RHS& rhs) { return {lhs, rhs}; }

template <class LHS, class RHS>
struct Or_t {
LHS lhs;
RHS rhs;

  operator bool () {
    bool b_lhs(lhs);
    bool b_rhs(rhs);
    return b_lhs || b_rhs;
  }
};
template <class LHS, class RHS> 
Or_t<LHS, RHS> Or(const LHS& lhs, const RHS& rhs) { return {lhs, rhs}; }

int main() {
  if (auto i = And(1, Or(0, 3))) {
    printf("%d %d %d\n", i.lhs, i.rhs.lhs, i.rhs.rhs);
  }
  return 0;
}

(Notez que cela perd l'évaluation du court-circuit.)

BCS
la source
Je suppose qu'il perd (ou desserre?)
Peter - Réintégrer Monica
5
Je pense que l'intention d'OP était la brièveté, la clarté et la maintenabilité du code. Vous avez proposé «solution» fait le contraire.
Dženan