Найелегантніший спосіб записати один кадр "якщо"


136

Оскільки на C ++ 17 можна записати ifблок, який буде виконуватися точно один раз так:

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

    }
}

Я знаю, що я можу це переосмислити, і є інші способи вирішити це, але все-таки - чи можна це написати якось так, так що немає потреби do_once = falseв кінці?

if (DO_ONCE) {
    // Do stuff
}

Я думаю про функцію помічника do_once(), що містить static bool do_once, але що робити, якщо я хотів використовувати ту саму функцію в різних місцях? Може це час і місце для #define? Я сподіваюся, що ні.


52
Чому б не просто if (i == 0)? Це досить зрозуміло.
SilvanoCerza

26
@SilvanoCerza Тому, що справа не в цьому. Цей if-блок може бути десь у якійсь функції, яка виконується кілька разів, а не в регулярному циклі
nada

8
можливо std::call_once, це варіант (він використовується для нанизування різьби, але все-таки виконує свою роботу).
fdan

25
Ваш приклад може бути поганим відображенням вашої реальної проблеми, яку ви нам не показуєте, але чому б просто не зняти функцію одноразового виклику з циклу?
rubenvb

14
Мені не прийшло в голову, що змінні, ініціалізовані в ifумовах, можуть бути static. Це розумно.
HolyBlackCat

Відповіді:


143

Використання std::exchange:

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

Ви можете скоротити зворотне значення значення істини:

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

Але якщо ви багато використовуєте це, не будьте фантазією, а натомість створіть обгортку:

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

І використовувати його так:

if (static Once once; once)

На змінну не слід посилатися поза умовою, тому ім'я не сильно купує нас. Беручи натхнення з інших мов, таких як Python, які надають особливого значення _ідентифікатору, ми можемо написати:

if (static Once _; _)

Подальші вдосконалення: скористайтеся розділом BSS (@Deduplicator), уникайте запису в пам'ять, коли ми вже запущено (@ShadowRanger), і дайте підказку передбачення гілки, якщо ви збираєтеся тестувати багато разів (наприклад, як у запитанні):

// 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
Я знаю, що макроси отримують багато ненависті в C ++, але це виглядає настільки проклято чисто: #define ONLY_ONCE if (static bool DO_ONCE_ = true; std::exchange(DO_ONCE_, false))Використовується як:ONLY_ONCE { foo(); }
Fibbles

5
я маю на увазі, якщо ви пишете "один раз" три рази, то використовуйте його більше, ніж три рази, якщо висловлювання - це варте
Алан,

13
Ця назва _використовується в багатьох програмних засобах для позначення трансляційних рядків. Очікуйте, що трапляться цікаві речі.
Саймон Ріхтер

1
Якщо у вас є вибір, віддайте перевагу початковому значенню статичного стану, що становить всі біти-нулі. Більшість форматів, що виконуються, містять довжину площі абсолютно нуля.
Deduplicator

7
Використання _для змінної було б не-Pythonic. Вам не потрібно використовувати _для змінних , які будуть посилання пізніше, тільки для зберігання значень , де ви повинні надати змінну , але вам не потрібно значення. Зазвичай він використовується для розпакування, коли вам потрібні лише деякі значення. (Є й інші випадки використання, але вони сильно відрізняються від випадку
викиду

91

Можливо, не найелегантніше рішення і ви не бачите жодного актуального if, але стандартна бібліотека насправді охоплює цей випадок:, див std::call_once.

#include <mutex>

std::once_flag flag;

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

Перевагою тут є те, що це безпечно для ниток.


3
Мені не було відомо про std :: call_once в цьому контексті. Але за допомогою цього рішення вам потрібно оголосити std :: Once_flag для кожного місця, де ви використовуєте цей std :: call_once, чи не так?
нада

11
Це працює, але не призначене для простого рішення, якщо ідея призначена для багатопотокових програм. Це зайвий набір для чогось простого, оскільки він використовує внутрішню синхронізацію - все для чогось, що вирішиться простою, якщо. Він не просив безпечного рішення для потоку.
Майкл Чурдакіс

9
@MichaelChourdakis Я згоден з вами, це надмірність. Однак, варто знати про це, і особливо знати про можливість виразити те, що ви робите ("виконайте цей код один раз"), а не ховати щось за менш читаним if-хитрістю.
лубгр

17
А, бачити call_onceмене означає, що ти хочеш щось зателефонувати один раз. Божевільний, я знаю.
Баррі

3
@SergeyA, оскільки він використовує внутрішню синхронізацію - все для чогось, що було б вирішено простою, якщо. Це досить ідіоматичний спосіб зробити щось інше, ніж те, що просили.
Гонки легкості на орбіті

52

C ++ має вбудований примітивний потік управління, який складається з "( перед блоком; умова; після блоку )":

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

Або хакітніше, але коротше:

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

Однак я вважаю, що будь-яку з методик, представлених тут, слід використовувати обережно, оскільки вони (поки що не є) дуже поширеними.


1
Мені подобається перший варіант (хоча - як і багато варіантів тут - він не є безпечним для ниток, тому будьте уважні). Другий варіант дає мені озноб (важче читати і може виконуватися будь-яку кількість разів тільки дві різьблення ... b == false: Thread 1Оцінює !bі входить в цикл, Thread 2Дорівнює !bі входить в цикл, Thread 1робить свої речі і залишає петлю для, установки b == falseдля !bтобто b = true... Thread 2робить свої речі і йде для циклу, установка b == trueна !bте є b = false, що дозволяє весь процес буде повторюватися до нескінченності)
CharonX

3
Я вважаю іронічним, що одне з найелегантніших рішень проблеми, де який-небудь код повинен бути виконаний лише один раз, - це цикл . +1
нада

2
Я б уникнув b=!b, це виглядає приємно, але ви насправді хочете, щоб значення було помилковим, тому b=falseнадайте перевагу.
yo

2
Майте на увазі, що захищений блок запуститься знову, якщо він вийде не локально. Це може бути навіть бажано, але воно відрізняється від усіх інших підходів.
Девіс

29

На C ++ 17 можна писати

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

щоб уникнути гри iв корпусі петлі. iпочинається з 0 (гарантованим стандартом), а вираз після ;множин iв 1перший раз вона обчислюється.

Зауважте, що в C ++ 11 ви можете досягти того ж за допомогою лямбда-функції

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

що також має незначну перевагу в тому, що iвін не просочується в корпус петлі.


4
Мені сумно сказати - якби це було поміщено в #define під назвою CALL_ONCE або так було б читабельніше
nada

9
Хоча, static int i;можливо, я справді не впевнений, що це один із тих випадків, коли iгарантовано буде ініціалізовано 0, тут використовувати набагато зрозуміліше static int i = 0;.
Кайл Вілмон

7
Незалежно від того, я погоджуюсь, ініціалізатор - це гарна ідея для розуміння
гонки

5
@Bathsheba Kyle цього не зробив, тому ваше твердження вже було доведено помилковим. Що вам коштує додати два символи заради чіткого коду? Давай, ти "головний архітектор програмного забезпечення"; ви повинні знати це :)
Гонки

5
Якщо ви думаєте, що написання початкового значення для змінної є протилежним ясним, або висловлюєте припущення про те, що "відбувається щось фанкі", я не думаю, що вам можуть допомогти;)
Гонки

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

Це рішення є безпечним для ниток (на відміну від багатьох інших пропозицій).


3
Чи знаєте ви ()необов’язково (якщо порожній) у лямбда-декларації?
Нонім

9

Ви можете зафіксувати одноразову дію в конструкторі статичного об'єкта, який ви інстанціюєте замість умовного.

Приклад:

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

Або ви дійсно можете дотримуватися макросу, який може виглядати приблизно так:

#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

Як сказав @damon, ви можете уникнути використання std::exchange, використовуючи ціле число, що зменшується, але ви повинні пам'ятати, що негативні значення вирішуються на істинні. Способом використання цього було б:

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

Переклад цього на вигадливу обгортку @ Acorn виглядатиме так:

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

Хоча використання, std::exchangeяк запропонував @Acorn, мабуть, самий ідіоматичний спосіб, операція обміну не обов'язково є дешевою. Хоча, звичайно, статична ініціалізація гарантовано є безпечною для потоків (якщо ви не скажете вашому компілятору цього не робити), тому будь-які міркування щодо продуктивності все одно дещо марні в присутностіstatic ключового слова.

Якщо ви стурбовані мікро-оптимізація (як люди , що використовують C ++ часто), ви могли б, а подряпати boolі використовувати intзамість цього, що дозволить використовувати постдекремент (або , вірніше, приріст , оскільки в відміну від boolдекремента intбуде НЕ насичувати до нуля ...):

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

Це раніше boolбуло операторів збільшення / зменшення, але вони давно застаріли (C ++ 11? Не впевнені?) І повинні бути повністю видалені в C ++ 17. Тим не менш, ви можете декрементуватиint справедливий штраф, і це, звичайно, буде спрацьовувати як булева умова.

Бонус: Ви можете реалізувати do_twiceабо do_thriceаналогічно ...


Я тестував це, і він спрацьовує кілька разів, за винятком першого разу.
нада

@nada: Дурне мені, ти маєш рацію ... виправив це. Він працював boolі декрементував один раз. Але приріст добре працює int. Дивіться демонстрацію в Інтернеті: coliru.stacked-crooked.com/a/ee83f677a9c0c85a
Деймон

1
У цій проблемі все ще виникає проблема, що вона може бути виконана стільки разів, що do_onceзагортається і в кінцевому підсумку знову потрапить на 0 (і знову і знову ...).
нада

Якщо точніше: тепер це буде виконуватися кожні INT_MAX рази.
нада

Ну так, але окрім того, що лічильник циклу також обертається в цьому випадку, навряд чи це проблема. Мало хто працює 2 мільярди (або 4 мільярди, якщо вони не підписані) ітерацій будь-чого взагалі. Якщо це так, вони все ще можуть використовувати 64-бітове ціле число. Використовуючи найшвидший доступний комп’ютер, ви помрете перед тим, як він завернеться, тому ви не можете подати до нього позов.
Деймон

4

На основі чудової відповіді на це @ Вірсавія - просто зробив це ще простіше.

В C++ 17, ви можете просто зробити:

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

(У попередніх версіях просто оголосити int iпоза блоком. Також працює в C :)).

Простими словами: ви оголошуєте i, що приймає за замовчуванням значення zero ( 0). Нуль - це фальси, тому ми використовуємо !оператор оклику ( ), щоб заперечувати це. Потім ми враховуємо приріст властивості<ID>++ оператора, який спочатку обробляється (призначається тощо), а потім збільшується.

Тому в цьому блоці я буду ініціалізований і матиме значення 0лише один раз, коли блок буде виконаний, і тоді значення збільшиться. Ми просто використовуємо !оператор, щоб заперечувати його.


1
Якби це було розміщено раніше, це, швидше за все, було б прийнятою відповіддю зараз. Дивовижне, дякую!
нада
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.