Подсмотрел вот в этом блог-посте - A New Approach to Build-Time Library Configuration - интересный трюк, который захотелось утащить к себе в склерозник на всякий случай.
Допустим, что мы сделали библиотеку. И допустим, что мы хотим дать пользователю как-то кастомизировать поведение библиотеки под себя во время компиляции.
Например, допустим, что мы используем в библиотеке какие-то временные вектора на стеке (скажем std::array<unsigned char, N>) и пользователь должен уметь управлять значением N. Чтобы он мог увеличить N когда это нужно или уменьшить N когда не нужно.
Традиционно в C++ для этих целей используют унаследованный из чистого Си подход с определением символов (они же define-ы). Например, определяют MY_LIB_DEFAULT_N через ключики компиляции. Что-то вроде:
g++ -D MY_LIB_DEFAULT_N=10240 ...
А в коде библиотеки мы делаем что-то вроде:
#if !defined(MY_LIB_DEFAULT_N) #define MY_LIB_DEFAULT_N 4096 #endif namespace my_lib { constexpr tmp_array_size = MY_LIB_DEFAULT_N; ... void some_func() { std::array<unsigned char, tmp_array_size> tmp_array; ... } } /* namespace my_lib */ |
Когда таких ручек для тонкой настройки много, все они собираются в некий user-config.h, который должен быть предоставлен пользователем библиотеки (а дефолтная версия user-config.h может генерироваться при установке библиотеки). Получается что-то вроде:
#define MY_LIB_DEFAULT_N 10240 #define MY_LIB_LOGGING_POLICY 0 #define MY_LIB_TRACING_MODE 3 ... |
В самой же библиотеке мы имеем специальный заголовочный файл impl/config.h, который будет иметь вид:
// Подключаем то, что выставил пользователь. #include "user-config.h" // А потом разбираемся с тем, что пользователь выставил или не выставил. #if !defined(MY_LIB_DEFAULT_N) #define MY_LIB_DEFAULT_N 4096 #endif #if !defined(MY_LIB_LOGGING_POLICY) #define MY_LIB_LOGGING_POLICY 2 #endif #if !defined(MY_LIB_TRACING_MODE) #define MY_LIB_TRACING_MODE 10 #endif ... // Далее преобразуем define-ы в типизированные константы. |
Для проверки наличия файла user-config.h, как показано в упомянутом выше блог-посте, можно использовать __has_include:
// Подключаем то, что выставил пользователь. #if __has_include("user-config.h" #include "user-config.h" #endif // А потом разбираемся с тем, что пользователь выставил или не выставил. #if !defined(MY_LIB_DEFAULT_N) #define MY_LIB_DEFAULT_N 4096 #endif #if !defined(MY_LIB_LOGGING_POLICY) #define MY_LIB_LOGGING_POLICY 2 #endif #if !defined(MY_LIB_TRACING_MODE) #define MY_LIB_TRACING_MODE 10 #endif ... // Далее преобразуем define-ы в типизированные константы. |
Но это Си-ное наследие. А ведь можно использовать и чисто C++ные механизмы.
Так, в нашем impl/config.h может быть:
namespace my_lib { ... namespace config_defaults { constexpr std::size_t tmp_array_size = 4096; constexpr logging_policy_t logging_policy = logging_policy_t::minimal; constexpr tracing_mode_t tracing_mode = tracing_mode_t::runtime_control; ... } /* namespace config_defaults */ using namespace config_defaults; // А вот и трюк. } /* namespace my_lib */ // Подключаем то, что выставил пользователь. #if __has_include("user-config.h" #include "user-config.h" #endif |
Теперь пользователь в своем user-config.h может написать, например, так:
namespace my_lib { constexpr std::size_t tmp_array_size = 10240; constexpr logging_policy_t logging_policy = logging_policy_t::detailed; constexpr tracing_mode_t tracing_mode = tracing_mode_t::off; ... } /* namespace my_lib */ |
И эти значения будут иметь больший приоритет, чем те, которые были определены в нашем my_lib::config_defaults, а затем введены в область видимости my_lib через using namespace.
Любопытно, что что-то похожее мы использовали в RESTinio для упрощения настройки свойств сервера (пример). Но у нас нужно было наследоваться от trait-класса. А тут без наследования, что для каких-то случаев гораздо удобнее.
Комментариев нет:
Отправить комментарий