Najbardziej elegancki sposób na jednorazowe „jeśli”

136

Od C ++ 17 można napisać ifblok, który zostanie wykonany dokładnie raz w ten sposób:

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

    }
}

Wiem, że mogę się nad tym zastanawiać i są inne sposoby rozwiązania tego problemu, ale nadal - czy można to jakoś napisać w ten sposób, więc do_once = falsena końcu nie ma potrzeby ?

if (DO_ONCE) {
    // Do stuff
}

Myślę o funkcji pomocniczej do_once()zawierającej static bool do_once, ale co jeśli chciałbym użyć tej samej funkcji w różnych miejscach? Czy to może być czas i miejsce na spotkanie #define? Mam nadzieję, że nie.

nada
źródło
52
Dlaczego nie po prostu if (i == 0)? To wystarczająco jasne.
SilvanoCerza
26
@SilvanoCerza Ponieważ nie o to chodzi. Ten blok-if może znajdować się gdzieś w jakiejś funkcji, która jest wykonywana wiele razy, a nie w regularnej pętli
nada.
8
może std::call_oncejest opcją (jest używana do gwintowania, ale nadal działa).
fdan
25
Twój przykład może być złym odzwierciedleniem twojego problemu z prawdziwego świata, którego nam nie pokazujesz, ale dlaczego nie po prostu usunąć z pętli funkcji jednorazowego wywołania?
rubenvb
14
Nie przyszło mi do głowy, że zmienne inicjowane w ifwarunkach mogą być static. To sprytne.
HolyBlackCat

Odpowiedzi:

143

Zastosowanie std::exchange:

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

Możesz go skrócić, odwracając wartość prawdy:

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

Ale jeśli często tego używasz, nie bądź wyszukany i zamiast tego utwórz opakowanie:

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

I używaj go jak:

if (static Once once; once)

Do zmiennej nie należy odwoływać się poza warunkiem, więc nazwa niewiele nas kupuje. Czerpiąc inspirację z innych języków, takich jak Python, które nadają _identyfikatorowi specjalne znaczenie , możemy napisać:

if (static Once _; _)

Dalsze ulepszenia: skorzystaj z sekcji BSS (@Deduplicator), unikaj zapisu do pamięci, gdy już uruchomiliśmy (@ShadowRanger) i daj wskazówkę dotyczącą przewidywania gałęzi, jeśli zamierzasz testować wiele razy (np. Jak w pytaniu):

// 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;
    }
};
Żołądź
źródło
32
Wiem, że makra mają dużo nienawiści w C ++, ale wygląda to tak cholernie czysto: #define ONLY_ONCE if (static bool DO_ONCE_ = true; std::exchange(DO_ONCE_, false))Do użycia jako:ONLY_ONCE { foo(); }
Fibbles
5
to znaczy, jeśli napiszesz „raz” trzy razy, to użyj tego więcej niż trzy razy w stwierdzeniach if, warto imo
Alan
13
Nazwa _jest używana w wielu programach do oznaczania przetłumaczalnych ciągów. Spodziewaj się ciekawych rzeczy.
Simon Richter
1
Jeśli masz wybór, wolisz, aby początkowa wartość stanu statycznego była równa zeru. Większość plików wykonywalnych zawiera długość obszaru zerowego.
Deduplicator
7
Użycie _zmiennej byłoby nie-Pythonic. Nie używasz _dla zmiennych, do których będziemy się później odwoływać, tylko do przechowywania wartości, w przypadku których musisz podać zmienną, ale nie potrzebujesz wartości. Zwykle jest używany do rozpakowywania, gdy potrzebujesz tylko niektórych wartości. (Istnieją inne przypadki użycia, ale są one zupełnie inne niż przypadek wartości jednorazowej).
jpmc26,
91

Może nie jest to najbardziej eleganckie rozwiązanie i nie widzisz żadnego rzeczywistego if, ale standardowa biblioteka faktycznie obejmuje ten przypadek: patrz std::call_once.

#include <mutex>

std::once_flag flag;

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

Zaletą jest to, że jest to bezpieczne dla wątków.

lubgr
źródło
3
Nie byłem świadomy std :: call_once w tym kontekście. Ale w przypadku tego rozwiązania musisz zadeklarować std :: once_flag dla każdego miejsca, w którym używasz tego std :: call_once, prawda?
nada
11
To działa, ale nie było przeznaczone do prostego rozwiązania, ale chodzi o aplikacje wielowątkowe. To przesada w przypadku czegoś tak prostego, ponieważ wykorzystuje wewnętrzną synchronizację - wszystko to w przypadku czegoś, co można by rozwiązać zwykłym „jeśli”. Nie prosił o rozwiązanie bezpieczne dla wątków.
Michael Chourdakis
9
@MichaelChourdakis Zgadzam się z tobą, to przesada. Jednak warto o tym wiedzieć, a zwłaszcza wiedzieć o możliwości wyrażenia tego, co robisz („wykonaj ten kod raz”), zamiast ukrywać coś za mniej czytelną sztuczką if.
lubgr
17
Uh, zobaczenie call_oncemnie oznacza, że ​​chcesz kiedyś do czegoś zadzwonić. Szalone, wiem.
Barry
3
@SergeyA, ponieważ wykorzystuje wewnętrzną synchronizację - wszystko do czegoś, co można rozwiązać za pomocą zwykłego if. To raczej idiomatyczny sposób robienia czegoś innego niż to, o co proszono.
Wyścigi lekkości na orbicie
52

C ++ ma wbudowaną operację podstawową przepływu sterowania, która już składa się z „( przed blokiem; warunek; po bloku )”:

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

Lub trudniejszy, ale krótszy:

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

Uważam jednak, że każdej z przedstawionych tu technik należy używać ostrożnie, ponieważ nie są (jeszcze?) Bardzo powszechne.

Sebastian Mach
źródło
1
Podoba mi się pierwsza opcja (choć - podobnie jak wiele wariantów tutaj - nie jest bezpieczna wątkowo, więc bądź ostrożny). Druga opcja przyprawia mnie o dreszcze (trudniejsza do odczytania i może być wykonywana dowolną ilość razy tylko z dwoma wątkami ... b == false: Thread 1oblicza !bi wchodzi do pętli for, Thread 2ocenia !bi wchodzi do pętli for, Thread 1wykonuje swoją pracę i pozostawia pętlę for, ustawiając b == falsena !bie b = true... Thread 2robi swoje i pozostawia pętlę for, ustawiając b == truena !bnp. b = false, pozwalając na powtarzanie całego procesu w nieskończoność)
CharonX
3
Uważam za ironiczne, że jednym z najbardziej eleganckich rozwiązań problemu, w którym jakiś kod ma być wykonany tylko raz, jest pętla . +1
nada
2
Unikałbym b=!b, to wygląda ładnie, ale tak naprawdę chcesz, aby wartość była fałszywa, więc b=falsepowinna być preferowana.
yo
2
Należy pamiętać, że chroniony blok zostanie uruchomiony ponownie, jeśli zakończy działanie nielokalnie. To może być nawet pożądane, ale różni się od wszystkich innych podejść.
Davis Herring,
29

W C ++ 17 możesz pisać

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

aby uniknąć zabawy z ikorpusem pętli. izaczyna się od 0 (gwarantowane przez standard), a wyrażenie po ;zestawach ido 1pierwszej oceny.

Zauważ, że w C ++ 11 możesz osiągnąć to samo dzięki funkcji lambda

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

co również ma niewielką zaletę polegającą na tym, że inie przecieka do korpusu pętli.

Batszeba
źródło
4
Przykro mi to mówić - gdyby to zostało umieszczone w #define o nazwie CALL_ONCE lub tak, byłoby bardziej czytelne
nada
9
Chociaż static int i;może (naprawdę nie jestem pewien) być jednym z tych przypadków, w których ina pewno zostanie zainicjowany 0, jest o wiele jaśniejszy w użyciu static int i = 0;tutaj.
Kyle Willmon
7
Niezależnie od tego, zgadzam się, że inicjator jest dobrym pomysłem na zrozumienie
Lightness Races in Orbit
5
@Bathsheba Kyle nie, więc twoje twierdzenie zostało już udowodnione fałszywie. Ile kosztuje dodanie dwóch znaków ze względu na czytelny kod? Daj spokój, jesteś „głównym architektem oprogramowania”; Powinieneś to wiedzieć :)
Lightness Races in Orbit
5
Jeśli myślisz, że przeliterowanie początkowej wartości zmiennej jest przeciwieństwem jasności lub sugeruje, że "dzieje się coś dziwnego", nie sądzę, że można by ci pomóc;)
Wyścigi lekkości na orbicie
14
static bool once = [] {
  std::cout << "Hello one-shot\n";
  return false;
}();

To rozwiązanie jest bezpieczne dla wątków (w przeciwieństwie do wielu innych sugestii).

Waxrat
źródło
3
Czy wiesz, że ()jest opcjonalne (jeśli puste) w deklaracji lambda?
Nonyme
9

Możesz opakować jednorazową akcję w konstruktorze statycznego obiektu, którego instancję tworzysz zamiast warunku.

Przykład:

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

Lub możesz trzymać się makra, które może wyglądać mniej więcej tak:

#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
źródło
8

Jak powiedział @damon, możesz uniknąć używania std::exchangezmniejszającej się liczby całkowitej, ale musisz pamiętać, że wartości ujemne są rozwiązywane na prawdę. Sposób na to byłby:

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

Tłumaczenie tego na fantazyjne opakowanie @ Acorn wyglądałoby tak:

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
źródło
7

O ile korzystanie std::exchangezgodnie z sugestią @Acorn jest chyba najbardziej idiomatycznym sposobem, operacja wymiany niekoniecznie jest tania. Chociaż oczywiście statyczna inicjalizacja jest gwarantowana jako bezpieczna wątkowo (chyba że powiesz kompilatorowi, aby tego nie robił), więc wszelkie rozważania dotyczące wydajności są i tak daremne w obecnościstatic słowa kluczowego.

Jeśli obawiasz się o mikro optymalizacji (jako osób korzystających z C ++ często są), można także zarysować booli używać intzamiast, który pozwoli Ci korzystać z post-ubytek (lub raczej przyrost , jak w przeciwieństwie boolzmniejszanie intbędzie nie nasycić do zera ...):

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

Kiedyś boolmiały operatory inkrementacji / dekrementacji, ale zostały one przestarzałe dawno temu (C ++ 11? Nie jesteś pewien?) I mają zostać całkowicie usunięte w C ++ 17. Niemniej jednak możesz zmniejszyćint sprawiedliwą karę i będzie to oczywiście działało jako warunek logiczny.

Bonus: możesz wdrożyć do_twicelub do_thricepodobnie ...

Damon
źródło
Przetestowałem to i odpala wiele razy, z wyjątkiem pierwszego.
nada
@nada: Głupi ja, masz rację ... poprawił to. Kiedyś działało booli zmniejszało się. Ale przyrost działa dobrze z int. Zobacz demo online: coliru.stacked-crooked.com/a/ee83f677a9c0c85a
Damon
1
Nadal ma ten problem, że może być wykonywany tak wiele razy, że do_oncezawija się i ostatecznie ponownie osiągnie 0 (i znowu i znowu ...).
nada
Aby być bardziej precyzyjnym: to byłoby teraz wykonywane co INT_MAX razy.
nada
Cóż, tak, ale oprócz licznika pętli również zawijającego się w tym przypadku, nie stanowi to problemu. Niewiele osób przeprowadza 2 miliardy (lub 4 miliardy, jeśli nie są podpisane) iteracji czegokolwiek. Jeśli tak, nadal mogą używać 64-bitowej liczby całkowitej. Korzystając z najszybszego dostępnego komputera, umrzesz, zanim się zapadnie, więc nie możesz zostać za to pozwany.
Damon
4

W oparciu o świetną odpowiedź @ Bathsheby - po prostu uczyniłem to jeszcze prostszym.

W C++ 17możesz po prostu:

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

(W poprzednich wersjach po prostu zadeklaruj int ipoza blokiem. Działa również w C :)).

W prostych słowach: deklarujesz i, które przyjmuje domyślną wartość zero ( 0). Zero to fałsz, dlatego używamy !operatora wykrzyknika ( ), aby go zanegować. Następnie bierzemy pod uwagę właściwość inkrementacji <ID>++operatora, która najpierw jest przetwarzana (przypisywana itp.), A następnie zwiększana.

Dlatego w tym bloku zostanie zainicjowany i będzie miał wartość 0tylko raz, kiedy blok zostanie wykonany, a następnie wartość wzrośnie. Po prostu używamy !operatora, aby to zanegować.

Nick L.
źródło
1
Gdyby to zostało opublikowane wcześniej, najprawdopodobniej byłaby to teraz zaakceptowana odpowiedź. Wielkie dzieki!
nada