Охранный код для контроля порядка включения модулей
Задача - есть ряд модулей, некоторые низкоуровневые, некоторые высокоуровневые, и хорошо бы контролировать порядок их включения, так, чтобы из системного модуля не зацепить модуль с бизнес-логикой.
Есть три модуля: модуль 1, модуль 2, модуль 3, которые расположены в порядке возрастания своего уровня. Модуль 3 может использовать код модулей 1 и 2, а модулю 2 разрешается пользоваться только модулем 1, но не модулем 3. Можно создать заголовочные файлы в этих модулях, примерно такого вида:
module1.h
#define LEVEL 1
#include "header.h"
... code
module2.h
#define LEVEL 2
#include "header.h"
... code
module3.h
#define LEVEL 3
#include "header.h"
... code
И всё было бы тривиально, если бы не ленивое разрешение макросов в C++. Если написать так, то оно запомнит не значение макроса LEVEL, а ссылку на него, и развернётся в итоге в код, который ничего не проверит.
#ifndef PREV_LEVEL
#define PREV_LEVEL 0
#endif
static_assert(PREV_LEVEL >= LEVEL, "Invalid include order");
#undef PREV_LEVEL
#define PREV_LEVEL LEVEL
Можно эту проблему решить с помощью буст-препроцессора:
header.h
#include <boost/preprocessor/slot/slot.hpp>
#define PREV_LEVEL BOOST_PP_SLOT(1)
#define BOOST_PP_VALUE LEVEL
#include BOOST_PP_ASSIGN_SLOT(1)
static_assert(PREV_LEVEL >= LEVEL, "Invalid include order");
И если в модуле 1 включить заголовок из модуля 2, то будет ошибка компиляции. Оно работает, но использовать его не хочется, ибо буст препроцессор - это какой-то оверкилл для такой задачи.
Как можно обойтись без буста? Первая идея, взять enable_if:
#ifndef GUARD_CHECK_CORE
#define GUARD_CHECK_CORE
namespace Guard
{
template <int T>
constexpr std::enable_if_t<0 <= T, bool> level_check()
{
return true;
}
}
#endif
namespace Guard
{
static_assert(level_check<LEVEL>(), "Invalid include order");
template <int T>
constexpr std::enable_if_t<T<LEVEL, bool> level_check()
{
return false;
}
}
Этот пример работает не совсем правильно, но в принципе, идея рабочая. Хотя, нагружать сборщик лишними вычислениями в процессе компиляции, это, наверное, даже хуже буст препроцессора.
И думается, что такие подходы вообще не совсем корректны - если мы в модуле 3 сначала подключим модуль 2, а потом модуль 1, то всё будет в порядке, а если в модуле 3 сначала подключим 1, а потом 2, то будет ошибка. Как-то это странно, обязывать выстраивать включения в файле, сообразно внешней иерархии модулей.
Можно ограничиться только значеним текущего модуля и впоследствии просто убеждаться, что мы не зацепили ничего из иерархии выше. А раз так, то достаточно сохранить только первое значение уровня, а затем его со всеми сравнивать. И в этом может помочь pop_macro
/push_macro
- расширение от Майрософта, которое поддерживают как минимум компиляторы “большой тройки”.
#ifndef ROOT_VALUE
#pragma push_macro("LEVEL")
#define ROOT_VALUE
#endif
static_assert(LEVEL <=
#pragma pop_macro("LEVEL")
#pragma push_macro("LEVEL")
LEVEL, "Invalid include order");
Код, конечно, непортируем на абстрактный компилятор, но мне и не надо, достаточно того, что clang, gcc и msvc это умеют.