Хочу представить небольшое готовое решение для тех, кому может понадобится написать большой (или не очень) кусок кода, который программа должна выполнить ровно 1 раз; причём поместить его может потребоваться куда угодно (в пределах разумного и правил синтаксиса C++). Если потребность в этом возникает чаще, чем пару раз во всём проекте, хорошо бы иметь на этот случай хоть какое-то более-менее работающее и, по возможности, не очень костыльное решение.

Сразу к делу


Не рассуждая долго, сразу выкладываю свой код, работающий со стандарта C++11 и выше.

#include <iostream>

#define DO_ONCE(...)  { static bool _do_once_ = ([&](){ __VA_ARGS__ }(), true); (void)_do_once_; }


void Foo(int val)
{
    using namespace std;
    // Имя _do_once_ никак не конфликтует с переменной в макросе DO_ONCE
    static unsigned int _do_once_ = 1;
    
    DO_ONCE
    (
       cout << "[First call of 'Foo' function]" << endl;
    )
    
    cout << "Calls: " << _do_once_++ << ", value: " << val << endl;
}

int main(int argc, char** argv)
{
    using namespace std;
    
    for (auto val : {1, 2, 3})
    {
        Foo(val);
        
        DO_ONCE
        (
            Foo(val);
        )
        
    }
    system("pause > nul");
    
    return 0;
}

/* Результат работы:
[First call of 'Foo' function]
Calls: 1, value: 1
Calls: 2, value: 1
Calls: 3, value: 2
Calls: 4, value: 3
/*

Рассмотрим самый важный кусок кода, который выполняет всю требуемую работу:

#define DO_ONCE(...)  { static bool _do_once_ = ([&](){__VA_ARGS__}(), true); (void)_do_once_; }

Выглядит не очень понятно и приятно, поэтому распишу чуть подробнее:

#define DO_ONCE(...)  {      static bool _do_once_ = ([&] ( ) { __VA_ARGS__ } ( ), true);      (void)_do_once_;  }

Работает так — в блоке кода создаётся локальная статическая переменная типа bool, которая, с помощью оператора «запятая», инициализируется в два этапа:

1. С помощью оператора «круглые скобки» вызывается лямбда:

[&] ( )
{
    __VA_ARGS__
}

которая захватывает по ссылке всё, что есть в области видимости и выполняет выражение, которое пользователь передал макросу DO_ONCE через аргументы, «упакованные» в __VA_ARGS__. Предустановленный макрос __VA_ARGS__ позволит сохранить весь код внутри блока, даже если в нём будут запятые (которые препроцессор на этапе разбора посчитает разделителями аргументов).

2. Переменной _do_once_ присваивается значение true (присваиваемое значение и тип самой переменной роли не играют, не считая занимаемый в программе размер). Запись "(void)_do_once_;" нужна чтобы избежать warning о неиспользуемой переменной.

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

Минусы подхода:

— Требует стандарт C++11,
— Требует создания 1 переменной на каждый блок DO_ONCE.

Плюсы:

— Хорошая читаемость, простой синтаксис.
— Нет ограничений на кол-во операторов в блоке и на их тип (не пытайтесь вписать туда break и continue, если цикл снаружи тела блока DO_ONCE или метку case, если DO_ONCE внутри switch).
— Возможность работать с переменными и функциями, доступными в области видимости вызова DO_ONCE без дополнительных затрат на передачу их в качестве аргументов.
— Нет риска получить ошибку переопределения переменной _do_once_, т.к. в теле блока оно просто замещает это имя из внешней области видимости.

Использованная литература:
» Лямбда-выражения
» Макрос со списками аргументов переменных
Поделиться с друзьями
-->

Комментарии (32)


  1. CodeRush
    07.11.2016 18:41
    -4

    Я что-то не понимаю, наверное, но с учетом ограничений на использование break и continue то же самое делается без лямд и C++ конструкцией do {… } while(0);


    1. CodeRush
      07.11.2016 18:55
      +1

      Именно что не понимаю, почитал стандарт, разобрался.


    1. MacIn
      07.11.2016 19:55
      +1

      Я что-то не понимаю
      Да. Это решение позволит выполнить произвольный участок кода один раз независимо от того, сколько раз будет вызвана функция, в которой он содержится. Этакий синглтон для участка кода.


  1. RPG18
    07.11.2016 19:10
    +22

    Так сильно не нравится std::call_once?


    1. stepanp
      07.11.2016 20:44
      +3

      У нее же фатальный недостаток, да и статью про нее не напишешь


      1. iOrange
        08.11.2016 23:37
        -1

        А что за фатальный недостаток? Почитал — вроде как раз самое то что нужно автору и потокобезопасно.


        1. stepanp
          09.11.2016 01:09
          +1

          Значит прочитайте еще про «фатальный недостаток» (https://ru.wikipedia.org/wiki/Синдром_неприятия_чужой_разработки)


  1. izvolov
    07.11.2016 21:33
    +2

    DO_ONCE
    (
        std::map<int, int> m;
        ...
    ); // Ошибка компиляции.


    1. woodser
      16.11.2016 20:52

      Верно, не учёл этого. Побочный эффект наличия запятых внутри вызова макроса. Устраняется так — в описании ставим переменное количество аргументов (...) вместо одного именованного, а вместо { body } пишем: __VA_ARGS__. Таким образом будут сохранены все аргументы, а так же — запятые между ними.


  1. xxvy
    08.11.2016 04:39

    Так-то можно, в некоторых случаях, и без C++11 обойтись

    void foo()
    {
        static class BAR {
            public: BAR() {
                //... однократный код
            }
        } bar;
    }
    


    Всё это дело тоже можно попробовать обернуть в макрос.

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


    1. RPG18
      08.11.2016 10:56
      +2

      Правда без модели памяти из C++11 эта штука не потоко-безопасная. Потоко-безопасная ленивая инициализация в C++


      1. xxvy
        08.11.2016 11:09

        Спасибо за ссылку. Весьма интересно.


  1. nckma
    08.11.2016 10:03
    -7

    Кажется люди уже безнадежно отравлены c++11/с++14.


    1. VioletGiraffe
      08.11.2016 11:47
      +4

      Если вы имеете в виду, что я сейчас могу написать пятью строчками из std то, для чего раньше нужен был бы буст и неделя на отладку (а на самом деле я просто не брался это писать) — да, я безнадёжно отравлен.


      1. nckma
        08.11.2016 14:37
        -1

        А у меня наоборот… одни программисты в Visual Studio написали чуток кода c++14 и теперь весь кросс платформенный проект не собирается для девайса, где gcc 4.8.3. Вся компания на ушах стоит. Первые говорят, что нужно делать апдейт компилятора, а вторые говорят, что вся система собрана проверенным временем 4.8.3 и ничего менять не будут, откатывайте свои изменения назад.
        А речь всего о десятке строк кода. Идет вторая неделя разбирательств, взаимных упреков, обид.


        1. VioletGiraffe
          08.11.2016 14:41
          +2

          Если проект — не однодневка по принципу «зарелизил и забыл», то я однозначно за обновление компилятора.
          С другой стороны, это право компании требовать, чтобы программеры писали код в рамках каких угодно ограничений. И право программистов уйти из такой уомпании и устроиться в другую. Не вижу повода для драмы. Я вот на С программировать ни за что не пойду, да и на С++03 уже вряд ли.


          1. nckma
            08.11.2016 16:11
            -1

            Речь на самом деле даже не о программировании.
            Вот такой пример:
            Есть сложная программа, которая состоит из многих модулей, которые писались разными людьми. Программа собирается через cmake/make.
            Программа использует Qt5.6.
            Ладно, проапгрейдили gcc. Все вроде бы счастливы и начинают использовать c++14. Ненадолго.
            Апгрейдим Qt до 5.7 и… о чудо… перестало собираться, потому, что cmake обнаружив Qt5.7 отчего-то устанавливает дополнительную опцию для нашего проекта --std=gnu++11, а люди уже вовсю используют c++14. А с Qt5.6 такого не было.
            Это только маленький эпизод. Когда программа сразу для многих платформ и разных устройств то неизбежно всплывают проблемы связанные со стандартами на компиляторы, окружение, совместимостью со старыми продуктами и т.д. интересно, что менеджеры такие «технические» вопросы не решают, а отдают на откуп программистам и тут уже кто во что горазд. Каждый же считает себя самым умным, хоть и работает над узким направлением и какой-то одной платформой.
            Мне кажется разумным не спешить с внедрением самых передовых фич. Немного консерватизма не повредит.


            1. VioletGiraffe
              08.11.2016 16:18
              +1

              Согласен насчёт самых передовых фич, но насколько разумно считать таковой С++14 в конце 2016 года?..
              А вот Qt 5.6 — хороший выбор, это LTS-версия c поддержкой до 2019 года. Если проект большой и сложность обновления версии высокая — тогда, может быть, и не стоит пока обновлять. У меня вот проекты относительно небольшие, я просто обновляю и смотрю, нет ли новых багов. То, что вы описали — флаги, изменения в окружении и т. д. — обычные рабочие моменты.


              1. nckma
                08.11.2016 16:31
                -1

                Насколько разумно для Qt5.7 выставлять флаги --std=gnu++11 в конце 2016 года?
                И да, эти рабочие моменты так выбивают из колеи, что сто раз задумаешься, а стоит ли новая фича этой головной боли? Когда делаешь изменение в проекте и с настороженностью ждешь оповещений от Teamcity, что в результате твоего фикса один из напрямую не связанных проектов не собрался.


                1. VioletGiraffe
                  08.11.2016 17:34
                  +3

                  Непонятно, почему gnu, а не std.
                  А поддержка С++14 включается добавлением в .pro-файл строки
                  CONFIG += c++14


        1. Satus
          08.11.2016 14:43
          +4

          То, что вы не можете поставить рабочий процесс в компании, не вина новых стандартов С++.
          Ну и таки раз в 3 года стоит задуматься как минимум насчёт обновления компилятора.


  1. monah_tuk
    08.11.2016 14:15
    +1

    Что-то вы не так далеко пошли. Если ваш вариант чууууточку исправить:


    #define DO_ONCE(callable)  { static bool _do_once_ = (callable(), true); (void)_do_once_; }

    то:


    1. появляется возможность передавать любой вызываемый объект: функцию, функтор, лямбду
    2. так как лямбда описывается теперь явно, то можно управлять тем, что будет захвачено
    3. появляется (с некоторыми ограничениями, о которых ниже) возможность применить для C++03/98

    По поводу последнего пункта: без модели памяти C++11 этот вариант не является потокобезопасным. Правда несколькими дополнительными движениями и это лечится.


    Ну и примеры вызовов: http://ideone.com/Dfr3zk


  1. maaGames
    08.11.2016 15:44
    +2

    А чем не устраивает «классика»:
    static bool do_once = true;
    if( do_once )
    {
    do_once = false;
    // code
    }

    При желании это, зачем-то, можно спрятать в макрос. Поддерживается даже в С.


    1. thedsi666
      08.11.2016 20:00
      +1

      Не thread safe


      1. Satus
        09.11.2016 03:18

        До С++11 вариант в статье тоже не thread-safe. А в С++11 есть std::call_once.


        1. monah_tuk
          09.11.2016 05:37

          Для пользования варианта с std::call_once в таком же виде придётся чуточку украсить и так же завернуть в макрос, что бы не было видно этого вспомогательного флага:


          #define  DO_ONCE(callable) do {    static std::once_flag _do_once_ ;   std::call_once(_do_once_, callable);  } while (false)

          или так:


          #define  DO_ONCE(callable, ...) do {    static std::once_flag _do_once_ ;   std::call_once(_do_once_, callable, __VA_ARGS__);  } while (false)

          Из плюсов варианта автора (в доработке для любого вызываемого объекта) — не нужно подключать дополнительно заголовочный файл :)


          1. Satus
            09.11.2016 14:07
            +1

            Только эти варианты (автора и call_once) не совсем равнозначны. Что будет, если функция кинет исключение? Call_once перебросит и не установит флаг, а что будет с оператором запятой?


      1. maaGames
        09.11.2016 15:23

        Понимаю, что все фломастеры разные, но для чего однократно выполнять некий код в многопоточном блоке? Т.е. этот код исполнится неизвестно когда и неизвестно кем, но один раз. Если это чисто для вызова внутри распараллеленного цикла, то абсолютно очевидно, что этот код никак не связан с контейнером и итерируемыми данными (или можно брать любой элемент контейнера), то этот однократный код с тем же успехом можно разместить перед циклом.
        В общем такая потребность больше смахивает на ошибку проектирования.


        1. thedsi666
          09.11.2016 15:50
          +1

          Не соглашусь. Допустим, есть несколько worker-тредов, которые в случайный момент обращаются к такому коду, и в нем в однократном блоке производится некая инициализация. Другое дело, что лично я не вижу смысла использовать для этого макросы, тем более когда есть стандартное средство.


          1. maaGames
            09.11.2016 16:04

            Типа странного синглтона получается нечто.


    1. xxvy
      09.11.2016 05:47

      хм. действительно. Ещё проще чем у меня. И даже зачем-то можно «break» и «continue» использовать.


  1. Chaos_Optima
    10.11.2016 17:06

    писал для себя както

    template<int RA, int RB>
    bool once()
    {
    	static bool once_val = true;
    	if (once_val)
    	{
    		once_val = false;
    		return true;
    	}
    	return once_val;
    }
    
    #define ONCE once<__COUNTER__, __COUNTER__>()
    


    используется соответственно
    if (ONCE)                          
    {                                  
    	....
    }