A maneira mais elegante de escrever um único 'se'

136

Desde o C ++ 17, é possível escrever um ifbloco que será executado exatamente uma vez assim:

#include <iostream>
int main() {
    for (unsigned i = 0; i < 10; ++i) {

        if (static bool do_once = true; do_once) { // Enter only once
            std::cout << "hello one-shot" << std::endl;
            // Possibly much more code
            do_once = false;
        }

    }
}

Eu sei que posso estar pensando demais nisso, e há outras maneiras de resolver isso, mas ainda assim - é possível escrever isso de alguma forma assim, para que não haja necessidade de do_once = falseno final?

if (DO_ONCE) {
    // Do stuff
}

Estou pensando em uma função auxiliar do_once(), contendo o static bool do_once, mas e se eu quisesse usar a mesma função em lugares diferentes? Pode ser esse o momento e o local para um #define? Espero que não.

nada
fonte
52
Por que não apenas if (i == 0)? Está claro o suficiente.
SilvanoCerza
26
@SilvanoCerza Porque esse não é o ponto. Este bloco se pode estar em algum lugar, de alguma função que é executada várias vezes, não em um loop normal
nada
8
talvez std::call_onceseja uma opção (é usada para segmentar, mas ainda funciona).
fdan 24/06/19
25
Seu exemplo pode ser um reflexo ruim do seu problema do mundo real que você não está nos mostrando, mas por que não tirar a função de chamada única uma vez fora do loop?
rubenvb
14
Não me ocorreu que variáveis ​​inicializadas em uma ifcondição poderiam ser static. Isso é esperto.
HolyBlackCat

Respostas:

143

Use std::exchange:

if (static bool do_once = true; std::exchange(do_once, false))

Você pode diminuir a reversão do valor verdade:

if (static bool do_once; !std::exchange(do_once, true))

Mas se você estiver usando muito isso, não seja chique e crie um wrapper:

struct Once {
    bool b = true;
    explicit operator bool() { return std::exchange(b, false); }
};

E use-o como:

if (static Once once; once)

A variável não deve ser referenciada fora da condição, portanto o nome não nos compra muito. Inspirando-se em outras linguagens como Python, que dão um significado especial ao _identificador, podemos escrever:

if (static Once _; _)

Outras melhorias: tire proveito da seção BSS (@Duplicator), evite a gravação na memória quando já executamos (@ShadowRanger) e dê uma dica de previsão de ramificação se você for testar várias vezes (por exemplo, como na pergunta):

// GCC, Clang, icc only; use [[likely]] in C++20 instead
#define likely(x) __builtin_expect(!!(x), 1)

struct Once {
    bool b = false;
    explicit operator bool()
    {
        if (likely(b))
            return false;

        b = true;
        return true;
    }
};
Bolota
fonte
32
Eu sei que as macros têm muito ódio em C ++, mas isso parece muito limpo: #define ONLY_ONCE if (static bool DO_ONCE_ = true; std::exchange(DO_ONCE_, false))Para ser usado como:ONLY_ONCE { foo(); }
Fibbles
5
Quer dizer, se você escrever "uma vez" três vezes, em seguida, usá-lo mais do que três vezes se as declarações que vale a pena imo
Alan
13
O nome _é usado em vários softwares para marcar seqüências de caracteres traduzíveis. Espere que coisas interessantes aconteçam.
Simon Richter
1
Se você tiver a opção, prefira que o valor inicial do estado estático seja zero de todos os bits. A maioria dos formatos executáveis ​​contém o comprimento de uma área totalmente zero.
Deduplicator
7
Usando _a variável, haveria não-Pythonic. Você não usa _para variáveis ​​que serão referenciadas posteriormente, apenas para armazenar valores nos quais você precisa fornecer uma variável, mas não precisa do valor. Geralmente é usado para descompactar quando você precisa apenas de alguns valores. (Há outros casos de uso, mas eles são muito diferente do caso valor descartável.)
jpmc26
91

Talvez não seja a solução mais elegante e você não veja nenhuma if, mas a biblioteca padrão realmente cobre esse caso :, veja std::call_once.

#include <mutex>

std::once_flag flag;

for (int i = 0; i < 10; ++i)
    std::call_once(flag, [](){ std::puts("once\n"); });

A vantagem aqui é que isso é seguro para threads.

lubgr
fonte
3
Eu não estava ciente de std :: call_once nesse contexto. Mas com esta solução, você precisa declarar um std :: once_flag para cada lugar que você usa esse std :: call_once, não é?
Nada
11
Isso funciona, mas não foi concebido para uma solução simples, se a idéia é para aplicativos multithread. É um exagero para algo tão simples, porque ele usa sincronização interna - tudo para algo que seria resolvido por um simples se. Ele não estava pedindo uma solução segura para threads.
Michael Chourdakis
9
@MichaelChourdakis Eu concordo com você, é um exagero. No entanto, vale a pena conhecer e, especialmente, saber sobre a possibilidade de expressar o que você está fazendo ("execute este código uma vez") em vez de esconder algo por trás de um truque menos legível.
lubgr
17
call_onceMe ver significa que você quer ligar uma vez. Louco, eu sei.
Barry
3
@SergeyA porque usa sincronização interna - tudo para algo que seria resolvido por um simples if. É uma maneira bastante idiomática de fazer algo diferente do que foi solicitado.
Lightness Races in Orbit
52

O C ++ possui uma primitiva de fluxo de controle integrada que consiste em "( antes do bloco; condição; pós-bloco )" já:

for (static bool b = true; b; b = false)

Ou mais hackier, mas mais curto:

for (static bool b; !b; b = !b)

No entanto, acho que qualquer uma das técnicas apresentadas aqui deve ser usada com cuidado, pois não são (ainda?) Muito comuns.

Sebastian Mach
fonte
1
Eu gosto da primeira opção (embora - como muitas variantes aqui - não seja segura para threads, tenha cuidado). A segunda opção me dá calafrios (mais difícil de ler e pode ser executado várias vezes com apenas dois threads ... b == false: Thread 1avalia !be entra no loop, Thread 2avalia !be entra no loop, Thread 1faz suas coisas e deixa o loop for, configurando b == falsecomo !bie b = true... Thread 2faz seu material e as folhas do loop for, definindo b == truea !bie b = false, permitindo que todo o processo deve ser repetido indefinidamente)
CharonX
3
Acho irônico que uma das soluções mais elegantes para um problema, em que algum código deva ser executado apenas uma vez, seja um loop . 1
nada
2
Eu evitaria b=!b, isso parece legal, mas você realmente deseja que o valor seja falso, portanto b=falsedeve ser preferido.
yo
2
Esteja ciente de que o bloco protegido será executado novamente se sair não localmente. Isso pode até ser desejável, mas é diferente de todas as outras abordagens.
Davis Herring
29

No C ++ 17 você pode escrever

if (static int i; i == 0 && (i = 1)){

para evitar brincar com io corpo do laço. icomeça com 0 (garantido pelo padrão) e a expressão após os ;conjuntos ipara 1a primeira vez que é avaliada.

Observe que no C ++ 11 você pode conseguir o mesmo com uma função lambda

if ([]{static int i; return i == 0 && (i = 1);}()){

o que também traz uma pequena vantagem, pois inão vaza para o corpo do loop.

Bathsheba
fonte
4
Estou triste dizer - se que seria colocado em um #define chamado CALL_ONCE ou então seria mais legível
nada
9
Embora static int i;possa (não tenho certeza) um desses casos em que ié garantido que seja inicializado 0, é muito mais claro usar static int i = 0;aqui.
Kyle Willmon
7
Independentemente Concordo um initialiser é uma boa idéia para a compreensão
Leveza raças em órbita
5
@Bathsheba Kyle não, então sua afirmação já foi provada falsa. Quanto custa você adicionar dois caracteres por uma questão de código claro? Vamos lá, você é um "arquiteto chefe de software"; você deve saber isso :)
Leveza raças em órbita
5
Se você acha que especificar o valor inicial de uma variável é o contrário de claro, ou sugere que "algo estranho está acontecendo", acho que não pode ser ajudado;)
Lightness Races in Orbit
14
static bool once = [] {
  std::cout << "Hello one-shot\n";
  return false;
}();

Esta solução é segura para threads (ao contrário de muitas das outras sugestões).

Waxrat
fonte
3
Você sabia que ()é opcional (se vazio) na declaração lambda?
Nonyme 01/07/19
9

Você pode agrupar a ação única no construtor de um objeto estático que você instancia no lugar da condicional.

Exemplo:

#include <iostream>
#include <functional>

struct do_once {
    do_once(std::function<void(void)> fun) {
        fun();
    }
};

int main()
{
    for (int i = 0; i < 3; ++i) {
        static do_once action([](){ std::cout << "once\n"; });
        std::cout << "Hello World\n";
    }
}

Ou você pode realmente ficar com uma macro, que pode ser algo como isto:

#include <iostream>

#define DO_ONCE(exp) \
do { \
  static bool used_before = false; \
  if (used_before) break; \
  used_before = true; \
  { exp; } \
} while(0)  

int main()
{
    for (int i = 0; i < 3; ++i) {
        DO_ONCE(std::cout << "once\n");
        std::cout << "Hello World\n";
    }
}
moooeeeep
fonte
8

Como o @damon disse, você pode evitar o uso std::exchangeusando um número inteiro decrescente, mas é preciso lembrar que os valores negativos são verdadeiros. A maneira de usar isso seria:

if (static int n_times = 3; n_times && n_times--)
{
    std::cout << "Hello world x3" << std::endl;
} 

Traduzir isso para o invólucro sofisticado da @ Acorn ficaria assim:

struct n_times {
    int n;
    n_times(int number) {
        n = number;
    };
    explicit operator bool() {
        return n && n--;
    };
};

...

if(static n_times _(2); _)
{
    std::cout << "Hello world twice" << std::endl;
}
Oreganop
fonte
7

Embora o uso std::exchangesugerido pelo @Acorn seja provavelmente a maneira mais idiomática, uma operação de troca não é necessariamente barata. Embora seja claro que a inicialização estática é garantida como segura para threads (a menos que você diga ao seu compilador para não fazê-lo), qualquer consideração sobre desempenho é algo fútil de qualquer maneira na presença da staticpalavra - chave.

Se você está preocupado com a micro-otimização (como costumam usar as pessoas que usam C ++), também pode boolusar e fazer scratch int, o que permitirá que você use o pós-decremento (ou melhor, o incremento , pois, ao contrário do booldecrementar de um int, não saturará zero) ...):

if(static int do_once = 0; !do_once++)

Costumava ter booloperadores de incremento / decremento, mas eles foram preteridos há muito tempo (C ++ 11? Não tenho certeza?) E devem ser totalmente removidos no C ++ 17. No entanto, você pode diminuir uma intmulta e, é claro, funcionará como uma condição booleana.

Bônus: Você pode implementar do_twiceou do_thricesimilarmente ...

Damon
fonte
Eu testei isso e ele dispara várias vezes, exceto pela primeira vez.
Nada
@nada: Parvo eu, você está certo ... corrigiu. Costumava trabalhar boole diminuir uma vez. Mas o incremento funciona bem int. Veja a demonstração on-line: coliru.stacked-crooked.com/a/ee83f677a9c0c85a
Damon
1
Isso ainda tem o problema de que ele pode ser executado tantas vezes que do_onceenvolve e eventualmente atinge 0 novamente (e novamente e novamente ...).
Nada
Para ser mais preciso: isso agora seria executado a cada INT_MAX vezes.
Nada
Bem, sim, mas para além do contador de loop também envolvendo em torno de, nesse caso, é quase um problema. Poucas pessoas executam 2 bilhões (ou 4 bilhões, se não assinados) de iterações de qualquer coisa. Se o fizerem, ainda poderão usar um número inteiro de 64 bits. Usando o computador mais rápido disponível, você morrerá antes que ele se espalhe, para que não possa ser processado por isso.
Damon
4

Com base na ótima resposta da @ Bathsheba para isso - simplificou ainda mais.

Em C++ 17, você pode simplesmente fazer:

if (static int i; !i++) {
  cout << "Execute once";
}

(Nas versões anteriores, basta declarar int ifora do bloco. Também funciona em C :)).

Em palavras simples: você declara i, que assume o valor padrão de zero ( 0). Zero é falsey, portanto, usamos o !operador de ponto de exclamação ( ) para negá-lo. Em seguida, levamos em conta a propriedade de incremento do <ID>++operador, que primeiro é processada (atribuída etc.) e depois incrementada.

Portanto, neste bloco, eu serei inicializado e só terá o valor 0uma vez, quando o bloco for executado, e o valor aumentará. Simplesmente usamos o !operador para negá-lo.

Nick L.
fonte
1
Se isso tivesse sido publicado anteriormente, provavelmente seria a resposta aceita agora. Incrível, obrigado!
Nada