Die eleganteste Art, ein One-Shot-If zu schreiben

136

Seit C ++ 17 kann man einen ifBlock schreiben , der genau einmal so ausgeführt wird:

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

    }
}

Ich weiß, dass ich das vielleicht überdenke, und es gibt andere Möglichkeiten, dies zu lösen, aber dennoch - ist es möglich, dies irgendwie so zu schreiben, so dass es do_once = falseam Ende nicht nötig ist ?

if (DO_ONCE) {
    // Do stuff
}

Ich denke an eine Hilfsfunktion, do_once() , die die enthält static bool do_once, aber was ist, wenn ich dieselbe Funktion an verschiedenen Orten verwenden möchte? Könnte dies die Zeit und der Ort für eine sein #define? Ich hoffe nicht.

Nada
quelle
52
Warum nicht einfach if (i == 0)? Es ist klar genug.
SilvanoCerza
26
@ SilvanoCerza Weil das nicht der Punkt ist. Dieser if-Block befindet sich möglicherweise irgendwo in einer Funktion, die mehrmals ausgeführt wird, nicht in einer regulären Schleife
nada
8
Vielleicht std::call_onceist es eine Option (es wird zum Einfädeln verwendet, macht aber trotzdem seinen Job).
fdan
25
Ihr Beispiel ist möglicherweise eine schlechte Widerspiegelung Ihres realen Problems, das Sie uns nicht zeigen, aber warum nicht einfach die Call-Once-Funktion aus der Schleife heben?
Rubenvb
14
Mir ist nicht in den Sinn gekommen, dass Variablen, die unter ifBedingungen initialisiert wurden , sein könnten static. Das ist schlau.
HolyBlackCat

Antworten:

143

Verwendung std::exchange:

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

Sie können es kürzer machen, indem Sie den Wahrheitswert umkehren:

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

Aber wenn Sie dies häufig verwenden, seien Sie nicht schick und erstellen Sie stattdessen einen Wrapper:

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

Und benutze es wie:

if (static Once once; once)

Die Variable sollte nicht außerhalb der Bedingung referenziert werden, daher kauft uns der Name nicht viel. Wir lassen uns von anderen Sprachen wie Python inspirieren , die dem _Bezeichner eine besondere Bedeutung geben , und schreiben möglicherweise:

if (static Once _; _)

Weitere Verbesserungen: Nutzen Sie den BSS-Abschnitt (@Deduplicator), vermeiden Sie das Schreiben des Speichers, wenn wir ihn bereits ausgeführt haben (@ShadowRanger), und geben Sie einen Hinweis zur Verzweigungsvorhersage, wenn Sie mehrmals testen möchten (z. B. wie in der Frage):

// 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;
    }
};
Eichel
quelle
32
Ich weiß, dass Makros in C ++ viel Hass bekommen, aber das sieht einfach so verdammt sauber aus: #define ONLY_ONCE if (static bool DO_ONCE_ = true; std::exchange(DO_ONCE_, false))Zu verwenden als:ONLY_ONCE { foo(); }
Fibbles
5
Ich meine, wenn Sie dreimal "einmal" schreiben, dann verwenden Sie es mehr als dreimal in if-Anweisungen, die es wert sind, imo
Alan
13
Der Name _wird in vielen Programmen verwendet, um übersetzbare Zeichenfolgen zu markieren. Erwarten Sie interessante Dinge.
Simon Richter
1
Wenn Sie die Wahl haben, ziehen Sie es vor, dass der Anfangswert des statischen Zustands alle Bits Null ist. Die meisten ausführbaren Formate enthalten die Länge eines Bereichs, der nur aus Null besteht.
Deduplikator
7
Bei Verwendung _der Variablen wäre unpythonisch. Sie verwenden nicht _für Variablen , die später Bezug genommen wird, nur zum Speichern von Werten , wo Sie haben eine Variable zur Verfügung zu stellen , aber sie nicht den Wert benötigen. Es wird normalerweise zum Auspacken verwendet, wenn Sie nur einige der Werte benötigen. (Es gibt andere Anwendungsfälle, aber sie unterscheiden sich
erheblich
91

Vielleicht nicht die eleganteste Lösung, und Sie sehen keine tatsächliche if, aber die Standardbibliothek deckt diesen Fall tatsächlich ab std::call_once.

#include <mutex>

std::once_flag flag;

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

Der Vorteil hierbei ist, dass dies threadsicher ist.

lubgr
quelle
3
Std :: call_once war mir in diesem Zusammenhang nicht bekannt. Aber mit dieser Lösung müssen Sie für jeden Ort, an dem Sie diesen std :: call_once verwenden, ein std :: Once_flag deklarieren, nicht wahr?
Nada
11
Dies funktioniert, war aber nicht für eine einfache Lösung gedacht, sondern für Multithread-Anwendungen. Es ist übertrieben für etwas so Einfaches, weil es interne Synchronisation verwendet - alles für etwas, das durch ein einfaches Wenn gelöst werden würde. Er bat nicht um eine thread-sichere Lösung.
Michael Chourdakis
9
@ MichaelChourdakis Ich stimme dir zu, es ist ein Overkill. Es lohnt sich jedoch, etwas zu wissen und insbesondere die Möglichkeit zu kennen, auszudrücken, was Sie tun ("diesen Code einmal ausführen"), anstatt etwas hinter einem weniger lesbaren Wenn-Trick zu verstecken.
lubgr
17
Äh, call_oncefür mich zu sorgen bedeutet, dass Sie einmal etwas anrufen möchten. Verrückt, ich weiß.
Barry
3
@SergeyA, weil es die interne Synchronisation verwendet - alles für etwas, das durch ein einfaches if gelöst werden würde. Es ist eine ziemlich idiomatische Art, etwas anderes zu tun als das, wonach gefragt wurde.
Leichtigkeitsrennen im Orbit
52

C ++ verfügt bereits über ein integriertes Kontrollflussprimitiv, das aus "( vor dem Block; Bedingung; nach dem Block )" besteht:

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

Oder hackiger, aber kürzer:

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

Ich denke jedoch, dass jede der hier vorgestellten Techniken mit Vorsicht angewendet werden sollte, da sie (noch?) Nicht sehr häufig sind.

Sebastian Mach
quelle
1
Ich mag die erste Option (obwohl sie - wie viele Varianten hier - nicht threadsicher ist, seien Sie also vorsichtig). Die zweite Option gibt mir Schüttelfrost (schwerer zu lesen und kann beliebig oft mit nur zwei Threads ausgeführt werden ... b == false: Thread 1wertet die !bSchleife aus und gibt sie ein , Thread 2wertet die !bSchleife aus und Thread 1gibt sie ein , erledigt ihre Aufgaben und verlässt die for-Schleife und setzt sie b == falseauf !bie b = true... Thread 2macht seine Sachen und verlässt die for-Schleife, setzt b == trueauf !bdh b = false, damit der gesamte Prozess auf unbestimmte Zeit wiederholt werden kann)
CharonX
3
Ich finde es ironisch, dass eine der elegantesten Lösungen für ein Problem, bei dem Code nur einmal ausgeführt werden soll, eine Schleife ist . +1
Nada
2
Ich würde vermeiden b=!b, das sieht gut aus, aber Sie möchten tatsächlich, dass der Wert falsch ist, also b=falsewerden Sie bevorzugt.
Yo
2
Beachten Sie, dass der geschützte Block erneut ausgeführt wird, wenn er nicht lokal beendet wird. Das mag sogar wünschenswert sein, unterscheidet sich aber von allen anderen Ansätzen.
Davis Herring
29

In C ++ 17 können Sie schreiben

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

um nicht iim Loop-Body herumzuspielen. ibeginnt mit 0 (vom Standard garantiert) und der Ausdruck nach den ;Sätzen wird izum 1ersten Mal ausgewertet.

Beachten Sie, dass Sie in C ++ 11 dasselbe mit einer Lambda-Funktion erreichen können

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

Dies hat auch den leichten Vorteil, dass ies nicht in den Schleifenkörper gelangt.

Bathseba
quelle
4
Ich bin traurig zu sagen - wenn das in eine #define namens CALL_ONCE oder so wäre es besser lesbar
nada
9
Während dies static int i;(ich bin mir wirklich nicht sicher) einer der Fälle sein kann, in denen ieine Initialisierung garantiert ist 0, ist die Verwendung static int i = 0;hier viel klarer .
Kyle Willmon
7
Unabhängig davon stimme ich zu, dass ein Initialisierer eine gute Idee zum Verständnis ist
Leichtigkeitsrennen im Orbit
5
@Bathsheba Kyle hat es nicht getan, daher hat sich Ihre Behauptung bereits als falsch erwiesen. Was kostet es Sie, zwei Zeichen hinzuzufügen, um einen klaren Code zu erhalten? Komm schon, du bist ein "Chef-Software-Architekt"; du solltest das wissen :)
Leichtigkeitsrennen im Orbit
5
Wenn Sie der Meinung sind, dass die Angabe des Anfangswertes für eine Variable das Gegenteil von klar ist oder darauf hindeutet, dass "etwas Ungewöhnliches passiert", kann Ihnen nicht geholfen werden;)
Leichtigkeitsrennen im Orbit
14
static bool once = [] {
  std::cout << "Hello one-shot\n";
  return false;
}();

Diese Lösung ist threadsicher (im Gegensatz zu vielen anderen Vorschlägen).

Waxrat
quelle
3
Wussten Sie, dass die ()Lambda-Deklaration optional ist (falls leer)?
Nonyme
9

Sie können die einmalige Aktion in den Konstruktor eines statischen Objekts einschließen, das Sie anstelle der Bedingung instanziieren.

Beispiel:

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

Oder Sie bleiben tatsächlich bei einem Makro, das ungefähr so ​​aussieht:

#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
quelle
8

Wie @damon sagte, können Sie die Verwendung vermeiden, std::exchangeindem Sie eine dekrementierende Ganzzahl verwenden, aber Sie müssen sich daran erinnern, dass negative Werte in true aufgelöst werden. Der Weg, dies zu benutzen, wäre:

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

Die Übersetzung in @ Acorns ausgefallenen Wrapper würde folgendermaßen aussehen:

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
quelle
7

Während die Verwendung std::exchangewie von @Acorn vorgeschlagen wahrscheinlich die idiomatischste Methode ist, ist eine Austauschoperation nicht unbedingt billig. Obwohl die statische Initialisierung garantiert threadsicher ist (es sei denn, Sie weisen Ihren Compiler an, dies nicht zu tun), sind Überlegungen zur Leistung bei Vorhandensein des staticSchlüsselworts ohnehin vergeblich .

Wenn Sie sich Gedanken über die Mikrooptimierung machen (wie dies bei C ++ häufig der Fall ist), können Sie diese auch kratzen boolund verwenden int, um die Nachdekrementierung (oder besser gesagt die Inkrementierung) zu verwenden , im Gegensatz zur boolDekrementierung und intwird nicht auf Null gesättigt ...):

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

Früher gab es boolInkrementierungs- / Dekrementierungsoperatoren, aber diese waren längst veraltet (C ++ 11? Nicht sicher?) Und müssen in C ++ 17 vollständig entfernt werden. Trotzdem können Sie eine intGeldstrafe dekrementieren , und es wird natürlich als boolesche Bedingung funktionieren.

Bonus: Sie können implementieren do_twiceoder do_thriceähnlich ...

Damon
quelle
Ich habe dies getestet und es wird bis auf das erste Mal mehrmals ausgelöst.
Nada
@nada: Blöd mich, du hast recht ... korrigiert. Früher arbeitete boolund dekrementierte es einmal. Aber Inkrement funktioniert gut mit int. Siehe Online-Demo: coliru.stacked-crooked.com/a/ee83f677a9c0c85a
Damon
1
Dies hat immer noch das Problem, dass es so oft ausgeführt wird, dass es sich do_onceumgibt und schließlich wieder 0 trifft (und immer wieder ...).
Nada
Genauer gesagt: Dies würde jetzt alle INT_MAX-Male ausgeführt.
Nada
Nun ja, aber abgesehen davon, dass der Schleifenzähler auch in diesem Fall herumläuft, ist es kaum ein Problem. Nur wenige Leute führen 2 Milliarden (oder 4 Milliarden, wenn nicht signierte) Iterationen von irgendetwas durch. In diesem Fall könnten sie immer noch eine 64-Bit-Ganzzahl verwenden. Wenn Sie den schnellsten verfügbaren Computer verwenden, werden Sie sterben, bevor er sich dreht, sodass Sie nicht dafür verklagt werden können.
Damon
4

Basierend auf @ Bathshebas großartiger Antwort darauf - hat es nur noch einfacher gemacht.

In C++ 17können Sie einfach tun:

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

(In früheren Versionen deklarieren Sie einfach int iaußerhalb des Blocks. Funktioniert auch in C :)).

In einfachen Worten: Sie deklarieren i, was den Standardwert Null ( 0) annimmt . Null ist falsch, daher verwenden wir den Ausrufezeichen ( !), um sie zu negieren. Wir berücksichtigen dann die Inkrement-Eigenschaft des <ID>++Operators, die zuerst verarbeitet (zugewiesen usw.) und dann inkrementiert wird.

Daher wird in diesem Block i initialisiert und hat den Wert 0nur einmal, wenn der Block ausgeführt wird, und dann erhöht sich der Wert. Wir benutzen einfach den !Operator, um es zu negieren.

Nick L.
quelle
1
Wenn dies früher gepostet worden wäre, wäre es jetzt höchstwahrscheinlich die akzeptierte Antwort. Super, danke!
Nada