C ++, deklaracja zmiennej w wyrażeniu „if”

114

Co tu się dzieje?

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.
}

Sekcja 6.4.3 standardu z 2003 r. Wyjaśnia, w jaki sposób zmienne zadeklarowane w warunku instrukcji wyboru mają zakres, który rozciąga się do końca podstacji kontrolowanych przez warunek. Ale nie widzę, gdzie mówi cokolwiek o braku możliwości umieszczenia nawiasów wokół deklaracji, ani nie mówi nic o tylko jednej deklaracji na warunek.

To ograniczenie jest irytujące nawet w przypadkach, gdy wymagane jest tylko jedno oświadczenie w warunku. Rozważ to.

bool a = false, b = true;

if(bool x = a || b)
{

}

Jeśli chcę wprowadzić zakres „if” -body z x ustawionym na false, wtedy deklaracja wymaga nawiasu (ponieważ operator przypisania ma niższy priorytet niż logiczne OR), ale ponieważ nie można użyć nawiasu, wymaga deklaracji x na zewnątrz ciała, przeciekając tę ​​deklarację do większego zakresu niż jest to pożądane. Oczywiście ten przykład jest trywialny, ale bardziej realistycznym przypadkiem byłby ten, w którym a i b są funkcjami zwracającymi wartości, które muszą zostać przetestowane

Czy to, co chcę zrobić, jest niezgodne ze standardem, czy też mój kompilator po prostu niszczy mi jaja (VS2008)?

Neutrino
źródło
6
„Jeśli chcę wejść w pętlę z” <- Twoje przykłady tak if. ifnie jest pętlą, jest warunkiem.
crashmstr
2
@crashmstr: true, ale warunki dla while są takie same jak dla if.
Mike Seymour,
2
Nie można tego zrobić za pomocą operatora przecinka? Mam na myśli if (int a = foo(), int b = bar(), a && b):? Jeśli operator przecinka nie jest przeciążony, standard mówi, że wyrażenia są obliczane od lewej do prawej, a wartość wyniku jest ostatnim wyrażeniem. Działa z forinicjalizacją pętli, dlaczego nie tutaj?
Archie
@Archie: Właśnie tego spróbowałem, nie mogłem zmusić tego do działania. Może możesz podać działający przykład?
James Johnston
@JamesJohnston: Właśnie próbowałem i to nie działa. Ten pomysł przyszedł mi prosto z głowy, zasugerowało mi, jak ifdziała, i wydaje się, że jest to błędne założenie.
Archie

Odpowiedzi:

63

Od C ++ 17 to, co próbowałeś zrobić, jest wreszcie możliwe :

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

Zwróć uwagę na użycie ;zamiast w ,celu oddzielenia deklaracji od stanu faktycznego.

fwyzard
źródło
23
Miły! Zawsze podejrzewałem, że wyprzedzam swój czas.
Neutrino
106

Myślę, że już wspomniałeś o tym problemie. Co kompilator powinien zrobić z tym kodem?

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

Operator „&&” to logiczne ORAZ zwarcie. Oznacza to, że jeśli pierwsza część (1==0)okaże się fałszywa, to drugiej części (bool a = false)nie należy oceniać, ponieważ już wiadomo, że ostateczna odpowiedź będzie fałszywa. Jeśli (bool a = false)nie jest oceniany, to co później zrobić z kodem, który używa a? Czy po prostu nie zainicjowalibyśmy zmiennej i pozostawilibyśmy ją niezdefiniowaną? Czy zainicjowalibyśmy go domyślnie? A co, jeśli typ danych był klasą i miałoby to niepożądane skutki uboczne? A co by było, gdybyś zamiast boolciebie użył klasy i nie miała ona domyślnego konstruktora takiego, że użytkownik musi podać parametry - co wtedy robimy?

Oto kolejny przykład:

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?

Wydaje się, że ograniczenie, które znalazłeś, wydaje się całkowicie uzasadnione - zapobiega występowaniu tego rodzaju niejasności.

James Johnston
źródło
1
Słuszna uwaga. Możesz wyraźnie wspomnieć o zwarciu, na wypadek gdyby OP lub inni go nie znali.
Chris Cooper,
7
Nie pomyślałem o tym. Chociaż w podanym przykładzie krótkie spięcie uniemożliwia wprowadzenie zakresu instrukcji warunkowej, w takim przypadku część wyrażenia, która deklaruje, że zmienna nie jest przetwarzana, nie jest problemem, ponieważ jej zakres jest ograniczony do zakresu instrukcji warunkowej. W którym przypadku nie byłoby lepiej, gdyby kompilator zgłosił błąd tylko w tych przypadkach, w których istnieje możliwość wprowadzenia zakresu instrukcji warunkowej, gdy część wyrażenia deklarującego zmienną nie została przetworzona? Co nie miało miejsca w podanych przeze mnie przykładach.
Neutrino
@Neutrino Na pierwszy rzut oka twój pomysł brzmi trochę jak problem SAT, który nie jest łatwy do rozwiązania, przynajmniej w ogólnym przypadku.
Christian Rau,
5
To, co wyjaśnisz o wszystkich problemach z posiadaniem wielu deklaracji zmiennych w warunku if oraz o tym, że można ich używać tylko w ograniczony sposób, sprawia, że ​​zastanawiam się, dlaczego ten rodzaj deklaracji został wprowadzony w pierwszej kolejności. Nigdy nie odczuwałem potrzeby takiej składni, zanim zobaczyłem ją w jakimś przykładzie kodu. Uważam, że ta składnia jest raczej niezgrabna i myślę, że zadeklarowanie zmiennej przed blokiem if jest znacznie bardziej czytelne. Jeśli naprawdę potrzebujesz ograniczyć zakres tej zmiennej, możesz umieścić dodatkowy blok wokół bloku if. Nigdy nie używałem tej składni.
Giorgio
2
Osobiście uważam, że byłoby raczej eleganckie, gdyby można było ograniczyć zakres używanych zmiennych do dokładnie zakresu bloku instrukcji, który musi ich używać, bez konieczności uciekania się do brzydkich środków, takich jak dodatkowe zagnieżdżone nawiasy klamrowe.
Neutrino
96

Warunek w instrukcji iflub whilemoże być wyrażeniem lub deklaracją pojedynczej zmiennej (z inicjalizacją).

Twój drugi i trzeci przykład nie są ani prawidłowymi wyrażeniami, ani prawidłowymi deklaracjami, ponieważ deklaracja nie może stanowić części wyrażenia. Chociaż byłoby przydatne móc napisać kod podobny do trzeciego przykładu, wymagałoby to znaczącej zmiany składni języka.

Nie widzę, gdzie mówi cokolwiek o niemożności umieszczenia nawiasów wokół deklaracji, ani nie mówi nic o tylko jednej deklaracji na warunek.

Specyfikacja składni w 6.4 / 1 podaje następujący warunek:

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

określając jedną deklarację, bez nawiasów ani innych ozdób.

Mike Seymour
źródło
3
Czy jest jakiś powód lub tło?
Tomáš Zato - Przywróć Monikę
23

Jeśli chcesz zawrzeć zmienne w węższym zakresie, zawsze możesz użyć dodatkowych { }

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

    if(bool x = a || b)
    {
        //...
    }
}//a and b are out of scope
crashmstr
źródło
5
+1. Dodatkowo przeniósłbym deklarację x do otaczającego bloku: dlaczego miałby mieć specjalny status w stosunku do a i b?
Giorgio,
1
Oczywiste, ale nieprzekonujące: to samo można powiedzieć o zwykłych zmiennych pętlowych. (To prawda, potrzeba ograniczonego zakresu zmiennej jest znacznie bardziej powszechna w pętlach.)
Peter - Przywróć Monikę
18

Ostatnia sekcja już działa, wystarczy napisać ją nieco inaczej:

if (int a = Func1())
{
   if (int b = Func2())
   {
        // do stuff with a and b
   }
}
Bo Persson
źródło
2

Oto brzydkie obejście za pomocą pętli (jeśli obie zmienne są liczbami całkowitymi):

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

Ale to zmyli innych programistów i jest to raczej zły kod, więc nie jest zalecany.

Prosty {}blok zamykający (jak już zalecamy) jest znacznie łatwiejszy do odczytania:

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

    if (a && b)
    {
        std::cout << "a = " << a << std::endl;
        std::cout << "b = " << b << std::endl;
    }
}
podstawowy6
źródło
1

Należy również zauważyć, że wyrażenia wewnątrz większego bloku if

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

niekoniecznie muszą być oceniane w sposób od lewej do prawej. Jeden dość subtelny błąd, który miałem kiedyś, był związany z faktem, że kompilator faktycznie testował od prawej do lewej zamiast od lewej do prawej.

DukeBrymin
źródło
5
Jednak obecnie C99 wymaga tego && i || są oceniane od lewej do prawej.
b0fh
2
Myślę, że ocena argumentów od prawej do lewej nigdy nie była możliwa z powodu logiki zwarcia. Zawsze był używany do takich rzeczy, jak test wskaźnika i pointee w jednym wyrażeniu, na przykład if(p && p->str && *p->str) .... Od prawej do lewej byłoby zabójcze i nie subtelne. Z innymi operatorami - nawet przydział! - i argumenty wywołań funkcji masz rację, ale nie z operatorami zwarć.
Peter - Przywróć Monikę
1

Dzięki odrobinie magii szablonów można w pewnym sensie obejść problem braku możliwości zadeklarowania wielu zmiennych:

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

(Uwaga, powoduje to utratę oceny zwarcia).

BCS
źródło
Przypuszczam, że przegrywa (lub rozluźnia się?)
Peter - Przywróć Monikę
5
Myślę, że intencją OP była zwięzłość kodu, przejrzystość i łatwość utrzymania. Zaproponowane przez Ciebie „rozwiązanie” działa odwrotnie.
Dženan