Самый элегантный способ написать однократное «если»


137

Начиная с 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

Ответы:


144

Используйте 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
Я имею в виду, если вы напишите «один раз» три раза, а затем используйте его более трех раз, если заявления того стоят, imo
— Алан

13
Имя _используется во многих программах для обозначения переводимых строк. Ожидайте, что интересные вещи случатся.
— Саймон Рихтер

1
Если у вас есть выбор, предпочтите начальное значение статического состояния равным нулю. Большинство исполняемых форматов содержат длину области с нулем.
— Дедупликатор

7
Использование _для переменной было бы не Pythonic. Вы не используете _переменные, на которые будут ссылаться позже, только для хранения значений, где вы должны предоставить переменную, но вам не нужно это значение. Обычно используется для распаковки, когда вам нужны только некоторые значения. (Существуют и другие варианты использования, но они довольно сильно отличаются от варианта одноразового значения.)
— jpmc26

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
Это работает, но не для простого решения, идея для многопоточных приложений. Это излишне для чего-то такого простого, потому что оно использует внутреннюю синхронизацию - все для чего-то, что было бы решено простым if. Он не просил о поточно-безопасном решении.
— Майкл Чурдакис

9
@MichaelChourdakis Я согласен с тобой, это перебор. Тем не менее, стоит знать и особенно знать о возможности выразить то, что вы делаете («выполнить этот код один раз»), вместо того, чтобы прятать что-то за менее читаемым трюком if.
— Lubgr

17
Увидеть call_onceменя означает, что ты хочешь позвонить однажды. Сумасшедший, я знаю.
— Барри

3
@SergeyA, потому что он использует внутреннюю синхронизацию - все для чего-то, что было бы решено простым if. Это довольно идиоматический способ сделать что-то, кроме того, о чем просили.
— Гонки

52

C ++ имеет встроенный примитив потока управления, который уже состоит из "( before-block; условие; after-block )":

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выполняет свою работу и покидает цикл for, устанавливая b == falseв !bie b = true... Thread 2выполняет свои функции и покидает цикл for, устанавливая b == trueв !bie b = false, что позволяет всему процессу повторяться бесконечно)
— CharonX

3
Я нахожу ироничным, что одним из самых элегантных решений проблемы, когда некоторый код должен выполняться только один раз, является цикл . +1
— нада

2
Я хотел бы избежать b=!b, это выглядит хорошо, но вы на самом деле хотите, чтобы значение было ложным, поэтому b=falseпредпочтительнее.
— лет»

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 Кайл не сделал, так что ваше утверждение уже было доказано ложным. Сколько стоит добавить два символа ради ясного кода? Давай, ты "главный архитектор программного обеспечения"; Вы должны знать это :)
— Гонки

5
Если вы думаете, что написание начального значения для переменной противоречит очевидному или предполагает, что «происходит что-то странное», я не думаю, что вам это поможет;)
— Гонки

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

Это решение является поточно-ориентированным (в отличие от многих других предложений).


3
Знаете ли вы, ()является необязательным (если он пуст) в лямбда-декларации?
— Nonyme

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, который принимает значение по умолчанию, равное нулю ( 0). Ноль - ложь, поэтому мы используем !оператор восклицательного знака ( ) для его отрицания. Затем мы принимаем во внимание свойство приращения<ID>++ оператора, который сначала обрабатывается (назначается и т. Д.), А затем увеличивается.

Следовательно, в этом блоке я буду инициализироваться и иметь значение 0только один раз, когда блок будет выполнен, а затем значение будет увеличиваться. Мы просто используем !оператор, чтобы отрицать его.


1
Если бы это было опубликовано ранее, скорее всего, это был бы принятый ответ сейчас. Круто, спасибо!
— Нада
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.