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

Начиная с 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, но что если я static bool do_once использовать эту функцию в разных местах? Может ли это быть время и место для #define? Надеюсь нет.

Ответы

Ответ 1

Используйте 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;
    }
};

Ответ 2

Возможно, это не самое элегантное решение, и вы не видите никакого фактического 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

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

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

Или хакер, но короче

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

Тем не менее, я думаю, что любой из методов, представленных здесь, следует использовать с осторожностью, поскольку они (пока?) Не очень распространены.

Ответ 4

В С++ 17 вы можете написать

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

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

Обратите внимание, что в С++ 11 вы можете достичь того же с помощью лямбда-функции

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

что также имеет небольшое преимущество в том, что i не просочился в тело циклы.

Ответ 5

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

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

Ответ 6

Вы можете поместить одноразовое действие в конструктор статического объекта, который вы создаете вместо условного.

Пример:

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

Ответ 7

Как сказал @damon, вы можете избежать использования std::exchange, используя уменьшающееся целое число, но вы должны помнить, что отрицательные значения принимают значение true. Способ использовать это будет:

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

Перевод этого в @Acorn fancy wrapper будет выглядеть так:

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

Ответ 8

Хотя использование 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 аналогично...

Ответ 9

На основании @Bathsheba отличный ответ для этого - просто сделал это еще проще.

В C++ 17 вы можете просто сделать:

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

(В предыдущих версиях просто объявляйте int i вне блока. Также работает в C :)).

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

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

Ответ 10

Самое простое решение - положить в do_once = false:

if (static bool do_once = true; do_once) { // enter only once
    do_once = false;

    // ...
}

Чтобы сделать решение "одним выстрелом", я бы использовал троичный:

// if 'do_once' is true, set it to false, and evaluate as true. Otherwise evaluate as false.
if (static bool do_once = true; (do_once ? (static_cast<void>(do_once = false), true) : false)) {

}

Это не потокобезопасно, но будет, если вы сделаете его static thread_local bool, и в этом случае он будет вызываться один раз для потока.

Чтобы заставить функцию делать это, вам понадобится уникальная "функция" для каждого оператора if, которая выглядит следующим образом, например, do_once<__COUNTER__>(). Или вы можете использовать статический экземпляр, который будет уникальным для каждого оператора if:

struct do_once {
    constexpr explicit operator bool() noexcept {
        if (flag) {
            flag = false;
            return true;
        }
        return false;
    }
private:
    bool flag = true;
};

int f() {
    if (static do_once _; _) {
        return 1;
    }
    return 0;
}

Ответ 11

Вот решение, которое не включает в себя дополнительные переменные, использует:

if (static bool do_once = true; do_once && !(do_once = false)) {
    std::cout << "hello one-shot" << std::endl;
}

Он просто злоупотребляет работой оператора &&. Первый раз, когда это называется true && true, оценивается (потому что do_once - true а правая сторона всегда true). Но поскольку правая сторона устанавливает для do_once значение false любой вызов после этого приводит к do_once false && true (что приводит к do_once false). Так что это if-выражение будет вызываться только один раз!

Ответ 12

Возможно, это не совсем то, что вы просите.

Не помещайте в цикл то, что вы хотели выполнить один раз. Просто оставьте их вне цикла. Кроме того, это будет быстрее, чем проверка некоторого однократного условия в цикле.

#include <iostream>
int main() {
    std::cout << "hello one-shot" << std::endl;
    // Possibly much more code
    for (unsigned i = 0; i < 10; ++i) {
        // code that was intended to be in the loop.
    }
}