Comments 34
Так сильно не нравится std::call_once?
DO_ONCE
(
std::map<int, int> m;
...
); // Ошибка компиляции.
Правда без модели памяти из C++11 эта штука не потоко-безопасная. Потоко-безопасная ленивая инициализация в C++
А речь всего о десятке строк кода. Идет вторая неделя разбирательств, взаимных упреков, обид.
С другой стороны, это право компании требовать, чтобы программеры писали код в рамках каких угодно ограничений. И право программистов уйти из такой уомпании и устроиться в другую. Не вижу повода для драмы. Я вот на С программировать ни за что не пойду, да и на С++03 уже вряд ли.
Вот такой пример:
Есть сложная программа, которая состоит из многих модулей, которые писались разными людьми. Программа собирается через cmake/make.
Программа использует Qt5.6.
Ладно, проапгрейдили gcc. Все вроде бы счастливы и начинают использовать c++14. Ненадолго.
Апгрейдим Qt до 5.7 и… о чудо… перестало собираться, потому, что cmake обнаружив Qt5.7 отчего-то устанавливает дополнительную опцию для нашего проекта --std=gnu++11, а люди уже вовсю используют c++14. А с Qt5.6 такого не было.
Это только маленький эпизод. Когда программа сразу для многих платформ и разных устройств то неизбежно всплывают проблемы связанные со стандартами на компиляторы, окружение, совместимостью со старыми продуктами и т.д. интересно, что менеджеры такие «технические» вопросы не решают, а отдают на откуп программистам и тут уже кто во что горазд. Каждый же считает себя самым умным, хоть и работает над узким направлением и какой-то одной платформой.
Мне кажется разумным не спешить с внедрением самых передовых фич. Немного консерватизма не повредит.
А вот Qt 5.6 — хороший выбор, это LTS-версия c поддержкой до 2019 года. Если проект большой и сложность обновления версии высокая — тогда, может быть, и не стоит пока обновлять. У меня вот проекты относительно небольшие, я просто обновляю и смотрю, нет ли новых багов. То, что вы описали — флаги, изменения в окружении и т. д. — обычные рабочие моменты.
И да, эти рабочие моменты так выбивают из колеи, что сто раз задумаешься, а стоит ли новая фича этой головной боли? Когда делаешь изменение в проекте и с настороженностью ждешь оповещений от Teamcity, что в результате твоего фикса один из напрямую не связанных проектов не собрался.
Что-то вы не так далеко пошли. Если ваш вариант чууууточку исправить:
#define DO_ONCE(callable) { static bool _do_once_ = (callable(), true); (void)_do_once_; }
то:
- появляется возможность передавать любой вызываемый объект: функцию, функтор, лямбду
- так как лямбда описывается теперь явно, то можно управлять тем, что будет захвачено
- появляется (с некоторыми ограничениями, о которых ниже) возможность применить для C++03/98
По поводу последнего пункта: без модели памяти C++11 этот вариант не является потокобезопасным. Правда несколькими дополнительными движениями и это лечится.
Ну и примеры вызовов: http://ideone.com/Dfr3zk
static bool do_once = true;
if( do_once )
{
do_once = false;
// code
}
При желании это, зачем-то, можно спрятать в макрос. Поддерживается даже в С.
Для пользования варианта с 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)
Из плюсов варианта автора (в доработке для любого вызываемого объекта) — не нужно подключать дополнительно заголовочный файл :)
В общем такая потребность больше смахивает на ошибку проектирования.
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)
{
....
}
#include <iostream>
template<typename T>
struct OnceRunner
{
OnceRunner(T&& func)
{
func();
}
};
template<typename T>
OnceRunner<T> const& RunOnce(T&& func)
{
static OnceRunner<T> const runner(std::move(func));
return runner;
};
void Foo()
{
std::cout << "Foo call" << std::endl;
RunOnce([]()
{
std::cout << "Run me once" << std::endl;
});
}
int main(int argc, char* argv[])
{
Foo();
Foo();
Foo();
return 0;
}
Результат:
Foo call
Run me once
Foo call
Foo call
При таком подходе можно вообще не задумываться об ограничениях на количество/состав операторов в коде инициализатора или о переопределнии неявных локальных переменных.
Код некорректен.
- Неправильная работа со сквозными ссылками (
move
вместоforward
); - Лишняя
rvalue
-ссылка там, где достаточно ссылки наconst rvalue
(в конструктореOnceRunner
); - Но самое главное, что это просто не работает, т.к. тип не задаёт однозначно функциональный объект, а значит, среди разных объектов одного и того же типа (например,
std::function
) вызван будет только первый.
Так что совет крайне вредный, и std::call_once
по-прежнему никто не отменял.
Однократно выполняющийся блок в произвольном месте кода (C++11 и выше)