La manière la plus élégante d'écrire un `` si '' unique

137

Depuis C ++ 17, on peut écrire un ifbloc qui sera exécuté exactement une fois comme ceci:

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

    }
}

Je sais que je réfléchis peut-être trop à cela, et il y a d'autres façons de résoudre cela, mais quand même - est-il possible d'écrire ceci d'une manière ou d'une autre comme ça, donc il n'y a pas besoin de do_once = falsela fin?

if (DO_ONCE) {
    // Do stuff
}

Je pense à une fonction d'assistance do_once(), contenant le static bool do_once, mais que faire si je voulais utiliser cette même fonction à différents endroits? Serait-ce le moment et l'endroit pour un #define? J'espère que non.

nada
la source
52
Pourquoi pas juste if (i == 0)? C'est assez clair.
SilvanoCerza
26
@SilvanoCerza Parce que ce n'est pas le but. Ce bloc if pourrait être quelque part dans une fonction qui est exécutée plusieurs fois, pas dans une boucle régulière
nada
8
std::call_onceest peut - être une option (elle est utilisée pour le filetage, mais fait toujours son travail)
fdan
25
Votre exemple peut être un mauvais reflet de votre problème réel que vous ne nous montrez pas, mais pourquoi ne pas simplement sortir la fonction d'appel unique de la boucle?
rubenvb
14
Il ne m'est pas venu à l'esprit que des variables initialisées dans une ifcondition pouvaient l'être static. C'est malin.
HolyBlackCat

Réponses:

144

Utilisez std::exchange:

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

Vous pouvez le raccourcir en inversant la valeur de vérité:

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

Mais si vous l'utilisez beaucoup, ne soyez pas sophistiqué et créez plutôt un wrapper:

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

Et utilisez-le comme:

if (static Once once; once)

La variable n'est pas censée être référencée en dehors de la condition, donc le nom ne nous achète pas beaucoup. En s'inspirant d'autres langages comme Python qui donnent une signification particulière à l' _identifiant, on peut écrire:

if (static Once _; _)

Autres améliorations: profitez de la section BSS (@Deduplicator), évitez l'écriture en mémoire lorsque nous avons déjà exécuté (@ShadowRanger), et donnez un indice de prédiction de branche si vous allez tester plusieurs fois (par exemple comme dans la question):

// 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;
    }
};
Gland
la source
32
Je sais que les macros suscitent beaucoup de haine en C ++, mais cela a l'air tellement propre: #define ONLY_ONCE if (static bool DO_ONCE_ = true; std::exchange(DO_ONCE_, false))à utiliser comme:ONLY_ONCE { foo(); }
Fibbles
5
je veux dire, si vous écrivez "une fois" trois fois, puis utilisez-le plus de trois fois dans les déclarations si cela en vaut la peine imo
Alan
13
Le nom _est utilisé dans de nombreux logiciels pour marquer des chaînes traduisibles. Attendez-vous à ce que des choses intéressantes se produisent.
Simon Richter
1
Si vous avez le choix, préférez que la valeur initiale de l'état statique soit tous bits-zéro. La plupart des formats exécutables contiennent la longueur d'une zone entièrement nulle.
Deduplicator
7
En utilisant _pour la variable, il y aurait un-Pythonic. Vous ne l'utilisez pas _pour les variables qui seront référencées plus tard, uniquement pour stocker des valeurs où vous devez fournir une variable mais vous n'avez pas besoin de la valeur. Il est généralement utilisé pour le déballage lorsque vous n'avez besoin que de certaines des valeurs. (Il existe d'autres cas d'utilisation, mais ils sont assez différents du cas de valeur
jetable
91

Peut-être pas la solution la plus élégante et vous n'en voyez aucune réelle if, mais la bibliothèque standard couvre en fait ce cas:, voir std::call_once.

#include <mutex>

std::once_flag flag;

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

L'avantage ici est que c'est thread-safe.

lubgr
la source
3
Je n'étais pas au courant de std :: call_once dans ce contexte. Mais avec cette solution, vous devez déclarer un std :: once_flag pour chaque endroit où vous utilisez ce std :: call_once, n'est-ce pas?
nada
11
Cela fonctionne, mais n'était pas destiné à une solution simple si, l'idée est pour les applications multithread. C'est exagéré pour quelque chose d'aussi simple car il utilise la synchronisation interne - le tout pour quelque chose qui serait résolu par un simple si. Il ne demandait pas une solution thread-safe.
Michael Chourdakis
9
@MichaelChourdakis Je suis d'accord avec vous, c'est exagéré. Cependant, cela vaut la peine de savoir, et surtout de savoir sur la possibilité d'exprimer ce que vous faites ("exécuter ce code une fois") au lieu de cacher quelque chose derrière une si-tromperie moins lisible.
lubgr
17
Euh, s'occuper call_oncede moi signifie que tu veux appeler quelque chose une fois. Fou, je sais.
Barry
3
@SergeyA parce qu'il utilise la synchronisation interne - tout cela pour quelque chose qui serait résolu par un simple if. C'est une façon plutôt idiomatique de faire autre chose que ce qui a été demandé.
Courses de légèreté en orbite le
52

C ++ a une primitive de flux de contrôle intégrée qui consiste déjà en "( before-block; condition; after-block )":

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

Ou plus hackeur, mais plus court:

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

Cependant, je pense que l'une des techniques présentées ici doit être utilisée avec précaution, car elles ne sont pas (encore?) Très courantes.

Sébastien Mach
la source
1
J'aime la première option (cependant - comme beaucoup de variantes ici - ce n'est pas thread-safe, alors soyez prudent). La deuxième option me donne des frissons (plus difficile à lire et peut être exécutée n'importe quel nombre de fois avec seulement deux threads ... b == false: Thread 1évalue !bet entre la boucle for, Thread 2évalue !bet entre la boucle for, Thread 1fait son truc et laisse la boucle for, mise b == falseà !bie b = true... Thread 2fait son truc et laisse la boucle for, mise b == trueà !bie b = false, permettant à tout le processus d'être répété indéfiniment)
CharonX
3
Je trouve ironique que l'une des solutions les plus élégantes à un problème, où un code est censé être exécuté une seule fois, soit une boucle . +1
nada le
2
J'éviterais b=!b, ça a l'air bien, mais vous voulez en fait que la valeur soit fausse, donc b=falsesera préférée.
le
2
Sachez que le bloc protégé s'exécutera à nouveau s'il se termine de manière non locale. Cela pourrait même être souhaitable, mais c'est différent de toutes les autres approches.
Davis Herring
29

En C ++ 17, vous pouvez écrire

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

afin d'éviter de jouer avec idans le corps de la boucle. icommence par 0 (garanti par la norme), et l'expression après les ;ensembles ijusqu'à 1la première évaluation.

Notez qu'en C ++ 11, vous pouvez réaliser la même chose avec une fonction lambda

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

ce qui présente également un léger avantage en ce qu'il ine fuit pas dans le corps de boucle.

Bathsheba
la source
4
Je suis triste de dire - si cela était mis dans une #define appelée CALL_ONCE ou alors, ce serait plus lisible
nada
9
Bien que cela static int i;puisse (je ne suis vraiment pas sûr) être l'un de ces cas où il iest garanti d'être initialisé 0, il est beaucoup plus clair à utiliser static int i = 0;ici.
Kyle Willmon
7
Quoi qu'il en soit, je suis d'accord, un initialiseur est une bonne idée pour la compréhension
Courses de légèreté en orbite
5
@Bathsheba Kyle ne l'a pas fait, donc votre affirmation a déjà été prouvée fausse. Combien cela vous coûte-t-il d'ajouter deux caractères pour un code clair? Allez, vous êtes un "architecte logiciel en chef"; vous devriez le savoir :)
Courses de légèreté en orbite
5
Si vous pensez que préciser la valeur initiale d'une variable est le contraire de clear, ou suggère que "quelque chose de génial se passe", je ne pense pas que vous puissiez être aidé;)
Courses de légèreté en orbite le
14
static bool once = [] {
  std::cout << "Hello one-shot\n";
  return false;
}();

Cette solution est thread-safe (contrairement à la plupart des autres suggestions).

Waxrat
la source
3
Saviez-vous que la ()déclaration lambda est facultative (si vide)?
Nonyme
9

Vous pouvez encapsuler l'action ponctuelle dans le constructeur d'un objet statique que vous instanciez à la place du conditionnel.

Exemple:

#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 vous pouvez en effet vous en tenir à une macro, qui peut ressembler à ceci:

#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
la source
8

Comme @damon l'a dit, vous pouvez éviter d'utiliser std::exchangeen utilisant un entier décrémentant, mais vous devez vous rappeler que les valeurs négatives se résolvent en vrai. La façon d'utiliser ceci serait:

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

Traduire ceci dans le wrapper sophistiqué de @ Acorn ressemblerait à ceci:

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
la source
7

Si utiliser std::exchangecomme suggéré par @Acorn est probablement le moyen le plus idiomatique, une opération d'échange n'est pas forcément bon marché. Bien que l'initialisation statique soit bien sûr sûre pour les threads (à moins que vous disiez à votre compilateur de ne pas le faire), toute considération concernant les performances est de toute façon quelque peu futile en présence du staticmot - clé.

Si vous êtes préoccupé par micro-optimisation (comme les personnes utilisant C ++ sont souvent), vous pouvez aussi bien gratter boolet utiliser à la intplace, ce qui vous permettra d'utiliser post-décrément (ou plutôt, incrément , comme la différence booldécrémentation un intsera pas sature à zéro ...):

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

Il fut un temps que boolHAD incrément / décrément opérateurs, mais ils ont été dépréciée depuis longtemps (C ++ 11? Pas sûr?) Et sont à éliminer complètement en C ++ 17. Néanmoins, vous pouvez décrémenter une intamende juste, et cela fonctionnera bien sûr comme une condition booléenne.

Bonus: vous pouvez mettre en œuvre do_twiceou de do_thricemanière similaire ...

Damon
la source
J'ai testé cela et il se déclenche plusieurs fois sauf pour la première fois.
nada
@nada: Silly moi, tu as raison ... corrigé. Il fonctionnait avec boolet décrémentait une fois. Mais l'incrément fonctionne bien avec int. Voir la démo en ligne: coliru.stacked-crooked.com/a/ee83f677a9c0c85a
Damon
1
Cela a toujours le problème qu'il pourrait être exécuté tant de fois qu'il do_onces'enroule et finira par atteindre 0 à nouveau (et encore et encore ...).
nada le
Pour être plus précis: ceci serait maintenant exécuté toutes les INT_MAX fois.
nada le
Eh bien oui, mais à part le compteur de boucles qui s'enroule également dans ce cas, ce n'est pas un problème. Peu de gens exécutent 2 milliards (ou 4 milliards si non signés) d'itérations de quoi que ce soit. S'ils le font, ils peuvent toujours utiliser un entier 64 bits. En utilisant l'ordinateur disponible le plus rapide, vous mourrez avant qu'il ne s'enroule, vous ne pouvez donc pas être poursuivi pour cela.
Damon le
4

Basé sur l'excellente réponse de @ Bathsheba à ce sujet, c'est encore plus simple.

Dans C++ 17, vous pouvez simplement faire:

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

(Dans les versions précédentes, déclarez simplement int i dehors du bloc. Fonctionne également en C :)).

En termes simples: vous déclarez i, qui prend la valeur par défaut de zéro ( 0). Zéro est faux, nous utilisons donc l' !opérateur point d'exclamation ( ) pour l'annuler. On prend alors en compte la propriété increment de la<ID>++ opérateur, qui est d'abord traitée (assignée, etc.) puis incrémentée.

Par conséquent, dans ce bloc, i sera initialisé et n'aura la valeur 0qu'une seule fois, lorsque le bloc sera exécuté, puis la valeur augmentera. Nous utilisons simplement l' !opérateur pour l'annuler.

Nick L.
la source
1
Si cela avait été publié plus tôt, ce serait probablement la réponse acceptée maintenant. Super merci!
nada