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

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

Преимущество здесь в том, что это потокобезопасно.

lubgr
источник
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;
}();

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

Waxrat
источник
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";
    }
}
moooeeeep
источник
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;
}
Oreganop
источник
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аналогичным образом ...

Damon
источник
Я проверил это, и он срабатывает несколько раз, за ​​исключением первого раза.
Нада
@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
Если бы это было опубликовано ранее, скорее всего, это был бы принятый ответ сейчас. Круто, спасибо!
Нада