Cara paling elegan untuk menulis satu jepretan 'jika'

138

Sejak C ++ 17 seseorang dapat menulis ifblok yang akan dieksekusi persis seperti ini:

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

    }
}

Saya tahu saya mungkin terlalu memikirkan ini, dan ada cara lain untuk menyelesaikannya, tetapi tetap - apakah mungkin untuk menulis ini dengan cara seperti ini, jadi tidak perlu do_once = falsebagian akhir?

if (DO_ONCE) {
    // Do stuff
}

Saya sedang memikirkan fungsi pembantu do_once(),, berisi static bool do_once, tetapi bagaimana jika saya ingin menggunakan fungsi yang sama di tempat yang berbeda? Mungkinkah ini waktu dan tempat untuk #define? Saya harap tidak.

nada
sumber
53
Kenapa tidak if (i == 0)? Cukup jelas.
SilvanoCerza
26
@SilvanoCerza Karena bukan itu intinya. If-block ini mungkin ada di suatu tempat di beberapa fungsi yang dijalankan beberapa kali, bukan dalam loop biasa
nada
8
mungkin std::call_onceadalah opsi (ini digunakan untuk threading, tetapi masih berfungsi).
fdan
25
Contoh Anda mungkin merupakan cerminan buruk dari masalah dunia nyata Anda yang tidak Anda tunjukkan kepada kami, tetapi mengapa tidak mengangkat saja fungsi panggil sekali saja?
rubenvb
15
Tidak terpikir oleh saya bahwa variabel yang diinisialisasi dalam suatu ifkondisi bisa jadi static. Itu pintar.
HolyBlackCat

Jawaban:

145

Penggunaan std::exchange:

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

Anda dapat mempersingkatnya dengan membalikkan nilai kebenaran:

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

Tetapi jika Anda sering menggunakan ini, jangan berlebihan dan buatlah pembungkus sebagai gantinya:

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

Dan gunakan seperti:

if (static Once once; once)

Variabel tidak seharusnya direferensikan di luar kondisi tersebut, sehingga nama tidak banyak membeli. Mengambil inspirasi dari bahasa lain seperti Python yang memberikan arti khusus pada _pengenalnya, kami dapat menulis:

if (static Once _; _)

Perbaikan lebih lanjut: manfaatkan bagian BSS (@Deduplicator), hindari penulisan memori ketika kita sudah menjalankan (@ShadowRanger), dan berikan petunjuk prediksi cabang jika Anda akan menguji berkali-kali (misalnya seperti dalam pertanyaan):

// 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;
    }
};
Biji pohon ek
sumber
33
Saya tahu makro mendapatkan banyak kebencian di C ++ tetapi ini terlihat sangat bersih: #define ONLY_ONCE if (static bool DO_ONCE_ = true; std::exchange(DO_ONCE_, false))Untuk digunakan sebagai:ONLY_ONCE { foo(); }
Fibbles
5
maksud saya, jika Anda menulis "sekali" tiga kali, maka gunakan lebih dari tiga kali jika pernyataan itu sangat berharga. Imo
Alan
13
Nama _ini digunakan di banyak perangkat lunak untuk menandai string yang dapat diterjemahkan. Harapkan hal-hal menarik terjadi.
Simon Richter
1
Jika Anda punya pilihan, lebih suka nilai awal status statis menjadi semua-bit-nol. Sebagian besar format yang dapat dieksekusi berisi panjang area semua-nol.
Deduplicator
7
Menggunakan _untuk variabel akan ada un-Pythonic. Anda tidak menggunakan _variabel yang akan direferensikan nanti, hanya untuk menyimpan nilai di mana Anda harus memberikan variabel tetapi Anda tidak memerlukan nilainya. Ini biasanya digunakan untuk membongkar ketika Anda hanya membutuhkan beberapa nilainya. (Ada kasus penggunaan lain, tetapi sangat berbeda dari kasus nilai sekali pakai.)
jpmc26
91

Mungkin bukan solusi yang paling elegan dan Anda tidak melihat yang sebenarnya if, tetapi pustaka standar sebenarnya mencakup kasus ini :, lihat std::call_once.

#include <mutex>

std::once_flag flag;

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

Keuntungannya di sini adalah benang ini aman.

lubgr
sumber
3
Saya tidak mengetahui std :: call_once dalam konteks itu. Tetapi dengan solusi ini Anda perlu mendeklarasikan std :: once_flag untuk setiap tempat Anda menggunakan std :: call_once itu, bukan?
nada
11
Ini berfungsi, tetapi tidak dimaksudkan untuk solusi if yang sederhana, idenya adalah untuk aplikasi multithread. Itu berlebihan untuk sesuatu yang sederhana karena menggunakan sinkronisasi internal - semuanya untuk sesuatu yang akan diselesaikan dengan sederhana jika. Dia tidak meminta solusi aman untuk benang.
Michael Chourdakis
9
@MichaelChourdakis Saya setuju dengan Anda, itu berlebihan. Namun, ini perlu diketahui, dan terutama mengetahui tentang kemungkinan untuk mengungkapkan apa yang Anda lakukan ("jalankan kode ini sekali") daripada menyembunyikan sesuatu di balik tipuan-if yang kurang terbaca.
lubgr
18
Uh, call_oncemelihatku berarti kamu ingin menelepon sesuatu sekali. Gila, saya tahu.
Barry
3
@SergeyA karena menggunakan sinkronisasi internal - semuanya untuk sesuatu yang akan diselesaikan dengan if. Ini adalah cara yang agak idiomatis untuk melakukan sesuatu selain dari apa yang diminta.
Balapan Ringan di Orbit
52

C ++ memang memiliki primitif aliran kontrol bawaan yang terdiri dari "( before-block; condition; after-block )" sudah:

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

Atau lebih hack, tapi lebih pendek:

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

Namun, menurut saya teknik apa pun yang disajikan di sini harus digunakan dengan hati-hati, karena tidak (belum?) Sangat umum.

Sebastian Mach
sumber
1
Saya suka opsi pertama (meskipun - seperti banyak varian di sini - ini tidak aman untuk benang, jadi berhati-hatilah). Opsi kedua membuat saya merinding (lebih sulit untuk dibaca dan dapat dieksekusi beberapa kali dengan hanya dua utas ... b == false: Thread 1mengevaluasi !bdan masuk untuk loop, Thread 2mengevaluasi !bdan masuk untuk loop, Thread 1melakukan tugasnya dan meninggalkan loop for, mengatur b == falseke !bie b = true... Thread 2melakukan tugasnya dan meninggalkan loop for, menyetel b == trueke !bie b = false, memungkinkan seluruh proses diulang tanpa batas)
CharonX
3
Saya merasa ironis bahwa salah satu solusi paling elegan untuk suatu masalah, di mana beberapa kode seharusnya dijalankan hanya sekali, adalah loop . +1
nada
2
Saya akan menghindari b=!b, itu terlihat bagus, tetapi Anda sebenarnya ingin nilainya salah, jadi b=falselebih disukai.
yo
2
Ketahuilah bahwa blok yang dilindungi akan berjalan kembali jika keluar secara non-lokal. Itu bahkan mungkin diinginkan, tetapi itu berbeda dari semua pendekatan lainnya.
Davis Herring
29

Di C ++ 17 Anda bisa menulis

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

untuk menghindari bermain-main dengan idi tubuh lingkaran. idimulai dengan 0 (dijamin oleh standar), dan ekspresi setelah ;disetel ike1 pertama kali dievaluasi.

Perhatikan bahwa di C ++ 11 Anda bisa mencapai hal yang sama dengan fungsi lambda

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

yang juga membawa sedikit keuntungan karena itidak bocor ke loop body.

Batsyeba
sumber
4
Saya sedih untuk mengatakan - jika itu dimasukkan ke dalam #define yang disebut CALL_ONCE atau lebih itu akan lebih mudah dibaca
nada
10
Meskipun static int i;mungkin (saya benar-benar tidak yakin) menjadi salah satu kasus di mana idijamin akan diinisialisasi 0, jauh lebih jelas untuk digunakan di static int i = 0;sini.
Kyle Willmon
8
Terlepas dari itu, saya setuju penginisialisasi adalah ide yang baik untuk pemahaman
Balapan Ringan di Orbit
5
@Bathsheba Kyle tidak melakukannya, jadi pernyataan Anda telah terbukti salah. Berapa biaya yang Anda keluarkan untuk menambahkan dua karakter demi kode yang jelas? Ayolah, Anda adalah "kepala arsitek perangkat lunak"; Anda harus tahu ini :)
Lightness Races di Orbit
6
Jika Anda berpikir mengeja nilai awal untuk variabel adalah kebalikan dari yang jelas, atau menunjukkan bahwa "sesuatu yang funky sedang terjadi", saya rasa Anda tidak dapat membantu;)
Lightness Races di Orbit
14
static bool once = [] {
  std::cout << "Hello one-shot\n";
  return false;
}();

Solusi ini aman untuk utas (tidak seperti banyak saran lainnya).

Waxrat
sumber
3
Tahukah Anda bahwa ()opsional (jika kosong) pada deklarasi lambda?
Nonyme
9

Anda bisa menggabungkan tindakan satu kali dalam konstruktor objek statis yang Anda buat sebagai pengganti kondisional.

Contoh:

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

Atau Anda mungkin tetap menggunakan makro, yang mungkin terlihat seperti ini:

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

Seperti yang dikatakan @damon, Anda dapat menghindari penggunaan std::exchangedengan menggunakan decrementing integer, tetapi Anda harus ingat bahwa nilai negatif berubah menjadi true. Cara menggunakannya adalah:

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

Menerjemahkan ini ke bungkus mewah @ Acorn akan terlihat seperti ini:

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

Meskipun menggunakan std::exchangeseperti yang disarankan oleh @Acorn mungkin adalah cara yang paling idiomatis, operasi pertukaran tidak selalu murah. Meskipun tentu saja inisialisasi statis dijamin aman untuk thread (kecuali jika Anda memberi tahu kompiler Anda untuk tidak melakukannya), jadi pertimbangan apa pun tentang kinerja agak sia-sia dengan adanyastatic kata kunci.

Jika Anda khawatir tentang pengoptimalan mikro (seperti yang sering dilakukan orang yang menggunakan C ++), Anda dapat menggaruk booldan menggunakan intsebagai gantinya, yang akan memungkinkan Anda untuk menggunakan penurunan pasca (atau lebih tepatnya, kenaikan , tidak seperti boolpenurunan yang tidakint akan jenuh ke nol ...):

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

Dulu booloperator tersebut memiliki operator increment / decrement, tetapi sudah lama tidak digunakan lagi (C ++ 11? Tidak yakin?) Dan harus dihapus seluruhnya dalam C ++ 17. Namun demikian, Anda dapat menurunkan fileint baik, dan itu tentu saja akan bekerja sebagai kondisi Boolean.

Bonus: Anda dapat menerapkan do_twiceatau do_thriceserupa ...

Damon
sumber
Saya menguji ini dan itu menyala beberapa kali kecuali untuk pertama kalinya.
nada
@nada: Saya konyol, Anda benar ... memperbaikinya. Dulu bekerja dengan booldan mengurangi sekali waktu. Tapi kenaikan berfungsi dengan baik int. Lihat demo online: coliru.stacked-crooked.com/a/ee83f677a9c0c85a
Damon
1
Ini masih memiliki masalah yang mungkin dieksekusi berkali-kali yang do_oncemembungkus dan akhirnya akan mencapai 0 lagi (dan lagi dan lagi ...).
nada
Lebih tepatnya: Ini sekarang akan dieksekusi setiap INT_MAX kali.
nada
Ya, tapi selain penghitung loop juga membungkus dalam kasus itu, itu hampir tidak menjadi masalah. Hanya sedikit orang yang menjalankan 2 miliar (atau 4 miliar jika tidak ditandatangani) iterasi apa pun. Jika ya, mereka masih dapat menggunakan integer 64-bit. Dengan menggunakan komputer tercepat yang tersedia, Anda akan mati sebelum komputer itu membungkusnya, jadi Anda tidak dapat dituntut untuk itu.
Damon
4

Berdasarkan jawaban bagus @ Batsyeba untuk ini - hanya membuatnya lebih sederhana.

Di C++ 17, Anda cukup melakukan:

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

(Di versi sebelumnya, cukup nyatakan di int iluar blok. Juga berfungsi di C :)).

Dengan kata sederhana: Anda mendeklarasikan i, yang mengambil nilai default nol ( 0). Nol adalah falsey, oleh karena itu kami menggunakan !operator tanda seru ( ) untuk meniadakannya. Kami kemudian memperhitungkan properti increment dari<ID>++ operator, yang pertama diproses (ditugaskan, dll) dan kemudian bertambah.

Oleh karena itu, di blok ini, saya akan diinisialisasi dan memiliki nilai 0 hanya satu kali, ketika blok dieksekusi, maka nilainya akan meningkat. Kami hanya menggunakan !operator untuk meniadakannya.

Nick L.
sumber
1
Jika ini telah diposting sebelumnya, kemungkinan besar itu akan menjadi jawaban yang diterima sekarang. Keren terima kasih!
nada