C ++, declaração de variável na expressão 'if'

113

O que está acontecendo aqui?

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

A seção 6.4.3 no padrão de 2003 expõe como as variáveis ​​declaradas em uma condição de instrução de seleção têm escopo que se estende até o final das subinstruções controladas pela condição. Mas não vejo onde diz algo sobre não ser capaz de colocar parênteses em torno da declaração, nem diz nada sobre apenas uma declaração por condição.

Essa limitação é irritante mesmo nos casos em que apenas uma declaração na condição é necessária. Considere isto.

bool a = false, b = true;

if(bool x = a || b)
{

}

Se eu quiser inserir o escopo de corpo 'if "com x definido como falso, então a declaração precisa de parênteses (uma vez que o operador de atribuição tem precedência menor do que o OR lógico), mas como o parêntese não pode ser usado, ele requer a declaração de x fora o corpo, vazando essa declaração para um escopo maior do que o desejado. Obviamente, este exemplo é trivial, mas um caso mais realista seria aquele em que aeb são funções que retornam valores que precisam ser testados

Então, o que eu quero fazer não está em conformidade com o padrão, ou meu compilador está me dando mal (VS2008)?

Neutrino
fonte
6
"Se eu quiser entrar no loop com" <- seus exemplos têm if. ifnão é um loop, é uma condicional.
crashmstr de
2
@crashmstr: true, mas as condições para while são as mesmas que para if.
Mike Seymour
2
Isso não pode ser feito com o operador vírgula? Quero dizer if (int a = foo(), int b = bar(), a && b):? Se o operador vírgula não estiver sobrecarregado, o padrão diz que as expressões são avaliadas da esquerda para a direita e o valor do resultado é a última expressão. Funciona com forinicialização de loops, por que não aqui?
Archie de
@Archie: Eu tentei isso, não consegui fazer funcionar. Talvez você possa fornecer um exemplo prático?
James Johnston
@JamesJohnston: Eu também tentei e não parece funcionar. Essa ideia surgiu do topo da minha cabeça, fui sugerido por como iffunciona, e parece ser uma suposição errada.
Archie de

Respostas:

63

A partir do C ++ 17, o que você estava tentando fazer finalmente é possível :

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

Observe o uso de ;em vez de ,para separar a declaração e a condição real.

Fwyzard
fonte
23
Agradável! Sempre suspeitei que estava à frente do meu tempo.
Neutrino
106

Acho que você já insinuou o problema. O que o compilador deve fazer com este código?

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

O operador "&&" é um AND lógico de curto-circuito. Isso significa que, se a primeira parte (1==0)for falsa, a segunda parte (bool a = false)não deve ser avaliada porque já se sabe que a resposta final será falsa. Se (bool a = false)não for avaliado, o que fazer com o código mais tarde que usa a? Será que não inicializaríamos a variável e deixá-la indefinida? Iríamos inicializá-lo com o padrão? E se o tipo de dados fosse uma classe e isso tivesse efeitos colaterais indesejáveis? E se em vez de boolvocê usar uma classe e ela não tiver um construtor padrão, de modo que o usuário deve fornecer parâmetros - o que fazemos então?

Aqui está outro exemplo:

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?

Parece que a limitação que você encontrou parece perfeitamente razoável - ela impede que esses tipos de ambigüidades aconteçam.

James Johnston
fonte
1
Bom ponto. Você pode querer mencionar o curto-circuito explicitamente, caso o OP ou outras pessoas não estejam familiarizados com ele.
Chris Cooper de
7
Eu não tinha pensado nisso. Embora no exemplo que você forneceu, o curto-circuito evita que o escopo da instrução condicional seja inserido, nesse caso a parte da expressão que declara que a variável não está sendo processada não é um problema, já que seu escopo é limitado ao da instrução condicional. Nesse caso, não seria melhor se o compilador gerasse um erro apenas nos casos em que há uma possibilidade de o escopo da instrução condicional ser inserido quando uma parte da expressão que declarou uma variável não foi processada? O que não foi o caso nos exemplos que dei.
Neutrino de
@Neutrino À primeira vista, sua ideia parece um pouco com o problema do SAT, que não é tão fácil de resolver, pelo menos no caso geral.
Christian Rau de
5
O que você explica sobre todos os problemas de ter várias declarações de variáveis ​​na condição if e o fato de que você só pode usá-las de forma restrita me faz pensar por que diabos esse tipo de declaração foi introduzido em primeiro lugar. Eu nunca havia sentido a necessidade de tal sintaxe antes de vê-la em algum exemplo de código. Acho essa sintaxe um tanto desajeitada e acho que declarar a variável antes do bloco if é muito mais legível. Se você realmente precisa restringir o escopo dessa variável, pode colocar um bloco extra ao redor do bloco if. Eu nunca usei essa sintaxe.
Giorgio de
2
Pessoalmente, acho que seria bastante elegante ser capaz de restringir o escopo das variáveis ​​que você está usando exatamente ao escopo do bloco de instrução que precisa usá-las, sem ter que recorrer a medidas feias como chaves de escopo aninhadas extras.
Neutrino de
96

A condição em uma instrução ifou whilepode ser uma expressão ou uma declaração de variável única (com inicialização).

Seu segundo e terceiro exemplos não são expressões válidas, nem declarações válidas, pois uma declaração não pode fazer parte de uma expressão. Embora seja útil ser capaz de escrever código como o seu terceiro exemplo, isso exigiria uma mudança significativa na sintaxe da linguagem.

Não vejo onde diz nada sobre não ser possível colocar parênteses em torno da declaração, nem diz nada sobre apenas uma declaração por condição.

A especificação de sintaxe em 6.4 / 1 fornece o seguinte para a condição:

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

especificando uma única declaração, sem parênteses ou outros adornos.

Mike Seymour
fonte
3
Existe alguma razão ou histórico para isso?
Tomáš Zato - Reintegração de Monica em
23

Se você deseja incluir variáveis ​​em um escopo mais restrito, você sempre pode usar { }

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

    if(bool x = a || b)
    {
        //...
    }
}//a and b are out of scope
crashmstr
fonte
5
+1. Além disso, eu moveria a declaração de x para o bloco circundante: por que deveria ter um status especial wrt a e b?
Giorgio
1
Óbvio, mas não convincente: o mesmo pode ser dito para variáveis ​​de loop comuns. (Concedido, a necessidade de escopo de variável limitado é muito mais comum em loops.)
Peter - Reintegrar Monica
18

A última seção já funciona, você só precisa escrever um pouco diferente:

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

Aqui está uma solução alternativa feia usando um loop (se ambas as variáveis ​​forem inteiras):

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

Mas isso vai confundir outros programadores e é um código bastante ruim, então não é recomendado.

Um {}bloco envolvente simples (como já recomendado) é muito mais fácil de ler:

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

    if (a && b)
    {
        std::cout << "a = " << a << std::endl;
        std::cout << "b = " << b << std::endl;
    }
}
basic6
fonte
1

Uma coisa a notar, também é que as expressões dentro do bloco if maior

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

não têm necessariamente a garantia de serem avaliados da esquerda para a direita. Um bug bastante sutil que tive naquela época tinha a ver com o fato de que o compilador estava testando da direita para a esquerda em vez de da esquerda para a direita.

DukeBrymin
fonte
5
Atualmente, no entanto, o C99 exige que && e || são avaliados da esquerda para a direita.
b0fh
2
Acho que a avaliação da direita para a esquerda dos argumentos nunca foi possível devido à lógica do curto-circuito. Sempre foi usado para coisas como um teste de ponteiro e ponta em uma única expressão, como if(p && p->str && *p->str) .... Da direita para a esquerda teria sido mortal e não sutil. Com outros operadores - até atribuição! - e argumentos de chamada de função você está certo, mas não com os operadores de curto-circuito.
Peter - Reintegrar Monica
1

Com um pouco de magia de modelo, você pode meio que contornar o problema de não ser capaz de declarar várias variáveis:

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

(Observe, isso perde a avaliação de curto-circuito.)

BCS
fonte
Suponho que perde (ou afrouxa?)
Peter - Reintegrar Monica
5
Acho que a intenção do OP era a brevidade, clareza e facilidade de manutenção do código. Você propôs "solução" que faz o oposto.
Dženan