Сразу к делу
Не рассуждая долго, сразу выкладываю свой код, работающий со стандарта 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)
RPG18
07.11.2016 19:10+22Так сильно не нравится std::call_once?
izvolov
07.11.2016 21:33+2DO_ONCE ( std::map<int, int> m; ... ); // Ошибка компиляции.
woodser
16.11.2016 20:52Верно, не учёл этого. Побочный эффект наличия запятых внутри вызова макроса. Устраняется так — в описании ставим переменное количество аргументов (...) вместо одного именованного, а вместо { body } пишем: __VA_ARGS__. Таким образом будут сохранены все аргументы, а так же — запятые между ними.
xxvy
08.11.2016 04:39Так-то можно, в некоторых случаях, и без C++11 обойтись
void foo() { static class BAR { public: BAR() { //... однократный код } } bar; }
Всё это дело тоже можно попробовать обернуть в макрос.
Правда на некоторых агрессивных настройках оптимизатора, этот код возможно будет выпилен. Не проверял.RPG18
08.11.2016 10:56+2Правда без модели памяти из C++11 эта штука не потоко-безопасная. Потоко-безопасная ленивая инициализация в C++
nckma
08.11.2016 10:03-7Кажется люди уже безнадежно отравлены c++11/с++14.
VioletGiraffe
08.11.2016 11:47+4Если вы имеете в виду, что я сейчас могу написать пятью строчками из std то, для чего раньше нужен был бы буст и неделя на отладку (а на самом деле я просто не брался это писать) — да, я безнадёжно отравлен.
nckma
08.11.2016 14:37-1А у меня наоборот… одни программисты в Visual Studio написали чуток кода c++14 и теперь весь кросс платформенный проект не собирается для девайса, где gcc 4.8.3. Вся компания на ушах стоит. Первые говорят, что нужно делать апдейт компилятора, а вторые говорят, что вся система собрана проверенным временем 4.8.3 и ничего менять не будут, откатывайте свои изменения назад.
А речь всего о десятке строк кода. Идет вторая неделя разбирательств, взаимных упреков, обид.VioletGiraffe
08.11.2016 14:41+2Если проект — не однодневка по принципу «зарелизил и забыл», то я однозначно за обновление компилятора.
С другой стороны, это право компании требовать, чтобы программеры писали код в рамках каких угодно ограничений. И право программистов уйти из такой уомпании и устроиться в другую. Не вижу повода для драмы. Я вот на С программировать ни за что не пойду, да и на С++03 уже вряд ли.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 такого не было.
Это только маленький эпизод. Когда программа сразу для многих платформ и разных устройств то неизбежно всплывают проблемы связанные со стандартами на компиляторы, окружение, совместимостью со старыми продуктами и т.д. интересно, что менеджеры такие «технические» вопросы не решают, а отдают на откуп программистам и тут уже кто во что горазд. Каждый же считает себя самым умным, хоть и работает над узким направлением и какой-то одной платформой.
Мне кажется разумным не спешить с внедрением самых передовых фич. Немного консерватизма не повредит.VioletGiraffe
08.11.2016 16:18+1Согласен насчёт самых передовых фич, но насколько разумно считать таковой С++14 в конце 2016 года?..
А вот Qt 5.6 — хороший выбор, это LTS-версия c поддержкой до 2019 года. Если проект большой и сложность обновления версии высокая — тогда, может быть, и не стоит пока обновлять. У меня вот проекты относительно небольшие, я просто обновляю и смотрю, нет ли новых багов. То, что вы описали — флаги, изменения в окружении и т. д. — обычные рабочие моменты.nckma
08.11.2016 16:31-1Насколько разумно для Qt5.7 выставлять флаги --std=gnu++11 в конце 2016 года?
И да, эти рабочие моменты так выбивают из колеи, что сто раз задумаешься, а стоит ли новая фича этой головной боли? Когда делаешь изменение в проекте и с настороженностью ждешь оповещений от Teamcity, что в результате твоего фикса один из напрямую не связанных проектов не собрался.VioletGiraffe
08.11.2016 17:34+3Непонятно, почему gnu, а не std.
А поддержка С++14 включается добавлением в .pro-файл строки
CONFIG += c++14
Satus
08.11.2016 14:43+4То, что вы не можете поставить рабочий процесс в компании, не вина новых стандартов С++.
Ну и таки раз в 3 года стоит задуматься как минимум насчёт обновления компилятора.
monah_tuk
08.11.2016 14:15+1Что-то вы не так далеко пошли. Если ваш вариант чууууточку исправить:
#define DO_ONCE(callable) { static bool _do_once_ = (callable(), true); (void)_do_once_; }
то:
- появляется возможность передавать любой вызываемый объект: функцию, функтор, лямбду
- так как лямбда описывается теперь явно, то можно управлять тем, что будет захвачено
- появляется (с некоторыми ограничениями, о которых ниже) возможность применить для C++03/98
По поводу последнего пункта: без модели памяти C++11 этот вариант не является потокобезопасным. Правда несколькими дополнительными движениями и это лечится.
Ну и примеры вызовов: http://ideone.com/Dfr3zk
maaGames
08.11.2016 15:44+2А чем не устраивает «классика»:
static bool do_once = true;
if( do_once )
{
do_once = false;
// code
}
При желании это, зачем-то, можно спрятать в макрос. Поддерживается даже в С.thedsi666
08.11.2016 20:00+1Не thread safe
Satus
09.11.2016 03:18До С++11 вариант в статье тоже не thread-safe. А в С++11 есть std::call_once.
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)
Из плюсов варианта автора (в доработке для любого вызываемого объекта) — не нужно подключать дополнительно заголовочный файл :)
Satus
09.11.2016 14:07+1Только эти варианты (автора и call_once) не совсем равнозначны. Что будет, если функция кинет исключение? Call_once перебросит и не установит флаг, а что будет с оператором запятой?
maaGames
09.11.2016 15:23Понимаю, что все фломастеры разные, но для чего однократно выполнять некий код в многопоточном блоке? Т.е. этот код исполнится неизвестно когда и неизвестно кем, но один раз. Если это чисто для вызова внутри распараллеленного цикла, то абсолютно очевидно, что этот код никак не связан с контейнером и итерируемыми данными (или можно брать любой элемент контейнера), то этот однократный код с тем же успехом можно разместить перед циклом.
В общем такая потребность больше смахивает на ошибку проектирования.thedsi666
09.11.2016 15:50+1Не соглашусь. Допустим, есть несколько worker-тредов, которые в случайный момент обращаются к такому коду, и в нем в однократном блоке производится некая инициализация. Другое дело, что лично я не вижу смысла использовать для этого макросы, тем более когда есть стандартное средство.
xxvy
09.11.2016 05:47хм. действительно. Ещё проще чем у меня. И даже зачем-то можно «break» и «continue» использовать.
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) { .... }
CodeRush
Я что-то не понимаю, наверное, но с учетом ограничений на использование break и continue то же самое делается без лямд и C++ конструкцией do {… } while(0);
CodeRush
Именно что не понимаю, почитал стандарт, разобрался.
MacIn