Il modo più elegante per scrivere un 'if' one-shot


137

Dal momento che C ++ 17 è possibile scrivere un ifblocco che verrà eseguito esattamente una volta in questo modo:

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

    }
}

So che potrei pensare troppo a questo, e ci sono altri modi per risolverlo, ma ancora - è possibile scrivere questo in qualche modo in questo modo, quindi alla fine non è necessario do_once = false?

if (DO_ONCE) {
    // Do stuff
}

Sto pensando a una funzione di supporto do_once(), contenente il static bool do_once, ma se volessi usare la stessa funzione in luoghi diversi? Potrebbe essere questo il momento e il luogo per un #define? Spero di no.


52
Perché non solo if (i == 0)? È abbastanza chiaro.
SilvanoCerza,

26
@SilvanoCerza Perché non è questo il punto. Questo if-block potrebbe trovarsi da qualche parte in una funzione che viene eseguita più volte, non in un ciclo normale
nada

8
forse std::call_onceè un'opzione (viene utilizzata per il threading, ma fa comunque il suo lavoro).
fdan,

25
Il tuo esempio potrebbe essere un riflesso negativo del tuo problema del mondo reale che non ci stai mostrando, ma perché non alzare il loop dalla funzione di chiamata una volta sola?
rubenvb,

14
Non mi è venuto in mente che ifpotrebbero essere variabili inizializzate in una condizione static. È intelligente.
HolyBlackCat,

Risposte:


144

Utilizzare std::exchange:

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

Puoi renderlo più breve invertendo il valore di verità:

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

Ma se lo usi molto, non essere sofisticato e crea un wrapper:

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

E usalo come:

if (static Once once; once)

La variabile non dovrebbe essere referenziata al di fuori della condizione, quindi il nome non ci compra molto. Prendendo ispirazione da altre lingue come Python che danno un significato speciale _all'identificatore, possiamo scrivere:

if (static Once _; _)

Ulteriori miglioramenti: approfitta della sezione BSS (@Deduplicator), evita la scrittura della memoria quando abbiamo già eseguito (@ShadowRanger) e dai un suggerimento sulla previsione del ramo se hai intenzione di testare più volte (ad es. Come nella domanda):

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

32
So che le macro ottengono molto odio in C ++ ma questo sembra così dannatamente pulito: #define ONLY_ONCE if (static bool DO_ONCE_ = true; std::exchange(DO_ONCE_, false))da usare come:ONLY_ONCE { foo(); }
Fibre

5
voglio dire, se scrivi "una volta" tre volte, quindi usalo più di tre volte se in dichiarazioni ne vale la pena imo
Alan

13
Il nome _viene utilizzato in molti software per contrassegnare stringhe traducibili. Aspettati che accadano cose interessanti.
Simon Richter,

1
Se hai la scelta, preferisci che il valore iniziale dello stato statico sia all-bit-zero. La maggior parte dei formati eseguibili contiene la lunghezza di un'area zero.
Deduplicatore,

7
Usando _per la variabile ci sarebbe un-Pythonic. Non utilizzare _per le variabili a cui verrà fatto riferimento in seguito, solo per memorizzare valori in cui è necessario fornire una variabile ma non è necessario il valore. In genere viene utilizzato per decomprimere quando sono necessari solo alcuni dei valori. (Esistono altri casi d'uso, ma sono piuttosto diversi dal caso del valore
usa

91

Forse non è la soluzione più elegante e non vedi alcun reale if, ma la libreria standard in realtà copre questo caso :, vedi std::call_once.

#include <mutex>

std::once_flag flag;

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

Il vantaggio qui è che questo è thread-safe.


3
Non ero a conoscenza di std :: call_once in quel contesto. Ma con questa soluzione devi dichiarare uno std :: once_flag per ogni posto in cui usi quello std :: call_once, vero?
nada,

11
Funziona, ma non era pensato per una semplice soluzione if, l'idea è per le applicazioni multithread. È eccessivo per qualcosa di così semplice perché utilizza la sincronizzazione interna - tutto per qualcosa che sarebbe risolto da un semplice if. Non stava chiedendo una soluzione thread-safe.
Michael Chourdakis,

9
@MichaelChourdakis Sono d'accordo con te, è eccessivo. Tuttavia, vale la pena conoscere e soprattutto conoscere la possibilità di esprimere ciò che stai facendo ("esegui questo codice una volta") invece di nascondere qualcosa dietro un if-inganno meno leggibile.
lubgr,

17
Uh, call_oncevedermi significa voler chiamare qualcosa una volta. Pazzo, lo so.
Barry,

3
@SergeyA perché utilizza la sincronizzazione interna - tutto per qualcosa che sarebbe risolto da un semplice if. È un modo piuttosto idiomatico di fare qualcosa di diverso da quello che è stato chiesto.
Razze di leggerezza in orbita

52

Il C ++ ha una primitiva del flusso di controllo integrata che consiste già di "( blocco precedente; condizione; blocco successivo )":

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

O hacker, ma più breve:

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

Tuttavia, penso che una qualsiasi delle tecniche presentate qui dovrebbe essere usata con cura, poiché non sono (ancora?) Molto comuni.


1
Mi piace la prima opzione (sebbene - come molte varianti qui - non è thread-safe, quindi fai attenzione). La seconda opzione mi dà i brividi (più difficile da leggere e può essere eseguito un numero qualsiasi di volte con solo due thread ... b == false: Thread 1valuta !bed entra in loop, Thread 2valuta !bed entra in loop, Thread 1fa le sue cose e lascia il ciclo for, impostando b == falsesu !bie b = true... Thread 2fa le sue cose e lascia il ciclo for, impostando b == truesu !bie b = false, permettendo a tutto il processo di essere ripetuto indefinitamente)
CharonX

3
Trovo ironico che una delle soluzioni più eleganti a un problema, in cui un codice dovrebbe essere eseguito una sola volta, sia un ciclo . +1
nada,

2
Eviterei b=!b, sembra carino, ma in realtà vuoi che il valore sia falso, quindi b=falsesarà preferito.
yo'

2
Tenere presente che il blocco protetto verrà eseguito nuovamente se esce non localmente. Potrebbe anche essere desiderabile, ma è diverso da tutti gli altri approcci.
Davis Herring,

29

In C ++ 17 puoi scrivere

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

per evitare di giocare con iil corpo del loop. iinizia con 0 (garantito dallo standard) e l'espressione dopo gli ;insiemi viene valutata iper 1la prima volta.

Si noti che in C ++ 11 è possibile ottenere lo stesso risultato con una funzione lambda

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

che porta anche un leggero vantaggio in quanto inon è trapelato nel corpo del circuito.


4
Sono triste a dirlo - se fosse inserito in un #define chiamato CALL_ONCE o così sarebbe più leggibile
nada

9
Mentre static int i;potrebbe (non sono veramente sicuro) essere uno di quei casi in cui iè garantito che sia inizializzato 0, è molto più chiaro da usare static int i = 0;qui.
Kyle Willmon,

7
Indipendentemente da ciò, concordo sul fatto che un inizialista sia una buona idea per la comprensione
gare di leggerezza in orbita,

5
@Bathsheba Kyle no, quindi la tua affermazione è già stata dimostrata falsa. Quanto costa aggiungere due caratteri per motivi di cancellazione del codice? Dai, sei un "capo architetto software"; dovresti saperlo :)
Lightness Races in Orbit,

5
Se pensi che sillabare il valore iniziale di una variabile sia il contrario di chiaro, o suggerisce che "sta succedendo qualcosa di strano", non penso che tu possa essere aiutato;)
Lightness Races in Orbit

14
static bool once = [] {
  std::cout << "Hello one-shot\n";
  return false;
}();

Questa soluzione è thread-safe (a differenza di molti altri suggerimenti).


3
Lo sapevi che ()è facoltativo (se vuoto) nella dichiarazione lambda?
Nonyme,

9

È possibile racchiudere l'azione una tantum nel costruttore di un oggetto statico che si crea un'istanza al posto del condizionale.

Esempio:

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

Oppure potresti davvero rimanere con una macro, che potrebbe assomigliare a questa:

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

8

Come ha detto @damon, puoi evitare di usare std::exchangeusando un numero intero decrescente, ma devi ricordare che i valori negativi si risolvono in vero. Il modo di utilizzare questo sarebbe:

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

Tradurre questo nel wrapper di fantasia di @ Acorn sarebbe simile al seguente:

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

7

Mentre l'utilizzo std::exchangecome suggerito da @Acorn è probabilmente il modo più idiomatico, un'operazione di scambio non è necessariamente economica. Sebbene, ovviamente, l'inizializzazione statica sia garantita come thread-safe (a meno che non si dica al compilatore di non farlo), quindi qualsiasi considerazione sulle prestazioni è in qualche modo futile in presenza della staticparola chiave.

Se siete preoccupati per micro-ottimizzazione (come le persone che utilizzano C ++ spesso sono), si potrebbe pure graffiare boole utilizzare intinvece, che vi permetterà di utilizzare post-decremento (o meglio, di incremento , come a differenza di booldecremento di intsi non saturare a zero ...):

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

In passato boolc'erano operatori di incremento / decremento, ma erano stati deprecati molto tempo fa (C ++ 11? Non sei sicuro?) E dovevano essere rimossi del tutto in C ++ 17. Tuttavia puoi decrementare una intmulta, e ovviamente funzionerà come una condizione booleana.

Bonus: è possibile implementare do_twiceo in do_thricemodo simile ...


Ho provato questo e si spara più volte tranne per la prima volta.
nada,

@nada: sciocco me, hai ragione ... corretto. E 'usato per il lavoro con boole decremento una volta. Ma l'incremento funziona bene con int. Guarda la demo online: coliru.stacked-crooked.com/a/ee83f677a9c0c85a
Damon,

1
Questo ha ancora il problema che potrebbe essere eseguito così tante volte che si do_onceavvolge e alla fine colpirà di nuovo 0 (e ancora e ancora ...).
nada,

Per essere più precisi: ora verrebbe eseguito ogni INT_MAX volte.
nada,

Bene sì, ma a parte il contatore del loop anche in questo caso, non è certo un problema. Poche persone eseguono 2 miliardi (o 4 miliardi se non firmati) di qualsiasi cosa. In tal caso, potrebbero comunque utilizzare un numero intero a 64 bit. Usando il computer più veloce disponibile, morirai prima che si avvolga, quindi non puoi essere citato in giudizio per questo.
Damon,

4

Basato sulla grande risposta di @ Bathsheba per questo, è stato ancora più semplice.

In C++ 17, puoi semplicemente fare:

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

(Nelle versioni precedenti, basta dichiarare int ifuori dal blocco. Funziona anche in C :)).

In parole semplici: dichiari i, che assume il valore predefinito zero ( 0). Zero è falso, quindi utilizziamo l' !operatore punto esclamativo ( ) per negarlo. Quindi prendiamo in considerazione la proprietà di incremento <ID>++dell'operatore, che viene prima elaborata (assegnata, ecc.) E quindi incrementata.

Pertanto, in questo blocco, sarò inizializzato e avrò il valore 0solo una volta, quando il blocco verrà eseguito, e quindi il valore aumenterà. Usiamo semplicemente l' !operatore per negarlo.


1
Se questo fosse stato pubblicato in precedenza, molto probabilmente sarebbe la risposta accettata ora. Fantastico, grazie!
nada,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.