Пару лет назад я написал статью про получение имен элементов enum в моих любимых плюсах без использования typeid, макросов и черной магии, а то и вообще в компайлтайм. Хотя нет, немного магии там все же было. Это был интересный опыт, но особого применения в проде я так и не нашел, хотя коллеги начали активно использовать эту возможность чтобы итерироваться по enum в поисках нужного элемента по его строковому представлению. Оно конечно задумывалось наоборот, но как говорится, пасту в тюбик обратно не запихнешь, пользуются и то радость. И тут в домашнем игровом движке мне понадобился похожий функционал получения имени структуры или класса в компайлтайм, можно конечно было сделать через typeid, но в релизной сборке rtti планируется отключать, так что этот вариант не подходит. А конвертировать имя структуры в строку все же хочется. При чем тут Гарри и для чего это все нужно в конце статьи.
И так как в домашнем проекте нет жестких рамком неиспользования стандартной библиотеки, то возможности готовых контейнеров вроде array/string_view сильно упростили код и вообще структуру всего решения. Получение имени типа в C++ та еще головная боль, казалось бы что известно компилятору на этапе сборки проекта должно легко доставаться какой-нибудь встроенной функцией вроде __builtin_typename, но этого как вы понимаете нет, и в грядущих стандартах тоже не предвится. Ближайший способ получить имя типа — это использовать std::type_info::name
, который на этапе компиляции не существует, потому что еще не собрана таблица типов, с которой эта структура работает. Да и вообще type_info
не гарантирует, что результат будет читаемым для человека.
Если вам интересно как я добрался до __PRETTY_FUNCTION__, об этом подробно рассписано в прошлой статье. Не буду повторяться, начну с того места, что возможность получить человеко-читаемую информацию в относительно любом месте кода есть. Если обернуть нужный тип в шаблонную функцию, добавив внутрь секретный вызов и сохранить куда-то результат, то имя типа прекрасно видно.
template <typename T>
constexpr std::string_view type_name_to_str() {
std::string_view function = __PRETTY_FUNCTION__;
std::string_view prefix = "constexpr std::string_view type_name_to_str() [with T = ";
std::string_view suffix = "]";
auto start = function.find(prefix) + prefix.size();
auto end = function.rfind(suffix);
return function.substr(start, end - start);
}
int main() {
std::cout << "Type name: " << type_name_to_str<int>() << std::endl;
std::cout << "Type name: " << type_name_to_str<double>() << std::endl;
std::cout << "Type name: " << type_name_to_str<std::string>() << std::endl;
}
Поиграться можно тут (godbolt), выхлоп кланга нам пока не интересен, главное что он тоже позволяет провернуть такой трюк. Отпиливание префикса и постфикса не слишком сложная задача, поэтому весь код приведен сразу. Отдельно напомню, то приведенная логика работает как с оптимизациями, так и без, это важно потому что компилятор мог запросто выкинуть ненужную информацию вроде той, что возвращает __PRETTY_FUNCTION__ в релизе, заменим имя типа первым попавшимся набором символов.
x86-64 gcc (trunk)
Program returned: 0
Program stdout
Type name: int; std::string_view = std::basic_string_view<char>
Type name: double; std::string_view = std::basic_string_view<char>
Type name: std::__cxx11::basic_string<char>; std::string_view = std::basic_string_view<char>
x86-64 clang (trunk)
Program returned: 139
Program stderr
terminate called after throwing an instance of 'std::out_of_range'
what(): basic_string_view::substr: __pos (which is 52) > __size (which is 42)
Program terminated
К сожалению, в C++17 нет способа создать constexpr
строку, поэтому придётся сделать это через std::array<char>, который умеет инициализироваться в компайлтайм. Но просто передать туда строку тоже не получится, потому что инициализация std::array происходит поэлементно, а вывод __PRETTY_FUNCTION__ это const char *
по факту. В строке, даже constexpr, можно обращаться к отдельным элементам по индексу. Если воспользоваться этим свойством, то можно разбить строку на отдельные символы, и далее полученную последоваться отправить в конструктор std::array.
Итак давайте соберем все вместе, и чтобы это все работало без простыни кода, воспользуемся умением компилятора автоматически выводить типы аргументов шаблона. А индексы сгенерируем через std::make_index_sequence<N>
, где N это длина изначальной строки. Массив здесь выступает в роли промежуточного хранилища, в конце которого идет терминальный символ, я не нашел с сожалению более красивого способа сформировать строку.
std::index_sequence<Idxs...> - здесь будут лежать индексы символов в строке
std::string_view - здесь будет лежать сами данных из __PRETTY_FUNCTION__
std::array - сюда через конструктор мы положим данные из строки поэлементно
template <size_t ... Idxs>
constexpr auto str_to_array(std::string_view str, std::index_sequence<Idxs...>) {
return std::array{ str[Idxs]..., '\0' };
}
Возвращаясь к самому первому примеру кода из статьи, можно его немного дописать, чтобы иметь возможность получать искомую строку в нормальном виде. (godbolt).
template <typename T>
constexpr auto type_name_str()
{
constexpr auto suffix = "]";
constexpr auto prefix = std::string_view{"with T = "};
constexpr auto function = std::string_view{__PRETTY_FUNCTION__};
constexpr auto start = function.find(prefix) + prefix.size();
constexpr auto end = function.rfind(suffix);
constexpr auto name = function.substr(start, (end - start));
return str_to_array(name, std::make_index_sequence<name.size()>{});
}
int main() {
std::cout << (char*)type_name_str<std::string>().data() << std::endl;
}
Очевидный минус такого решения это неудобный синтаксис вызова, чтобы приблизить его к синтаксису стандартной библиотеки и помочь компилятору закешировать уже найденные типы надо добавить синтаксического сахара и привести к более привычному виду (godbolt)
template <typename T>
struct type_name_holder {
static inline constexpr auto value = type_name_str<T>();
};
template <typename T>
constexpr std::string_view type_name() {
constexpr auto& value = type_name_holder<T>::value;
return std::string_view{value.data(), value.size()};
}
int main() {
std::cout << type_name<std::string>() << std::endl;
}
А зачем вообще это надо?
В свободное время я восстанавливаю игру и движок старенького ситибилдера Pharaoh, добрался наконец до интерфейса советников, и тут, уважаемый хабражитель, мне захотелось странного - авторегистрации классов окон советников и перегрузки интерфейса, да и вообще любых свойств, на лету в рантайме. Хотрелоад плюсовых структур из конфигов выходит за рамки этой статьи, но для того, чтобы знать свойства какого типа изменились надо иметь имя этого типа гдето в полях класса. Можно делать это например руками, как-то так, поначалу так и было:
namespace ui {
struct advisor_ratings_window : public advisor_window {
static constexpr inline const char * TYPEN = "advisor_ratings_window";
virtual int handle_mouse(const mouse *m) override { return 0; }
...
Воспользовавшись описанным выше кодом, можно сделать меньше ручной работы и привести это к виду.
struct advisor_window : public ui::widget {
bstring128 section;
advisor_window(pcstr s) : section(s) {
...
template<typename T>
struct advisor_window_t : public advisor_window {
inline advisor_window_t() : advisor_window(type_name<T>().data()) {
...
struct advisor_ratings_window : public advisor_window_t<advisor_ratings_window> {
virtual int handle_mouse(const mouse *m) override
...
В итоге получаем структуру, которая с минимальным ручным вмешательством хранит свой тип, по которому мы можем её ассоциировать в конфигах игры, и например поддерживать хотрелоад по имени типа.
advisor_ratings_window = {
ui : {
background : outer_panel({size:[40, 27]}),
background_image : image({pack:PACK_UNLOADED, id:2, pos :[60, 38]}),
...
Скрытый текст
Кстати если кто помнит Zeus: Master of Olympus game
то для него недавно тоже открыли open-source port, скрестил пальцы, чтобы ребятам хватило терпения после 5 лет продолжить работу над проектом.
При чем тут Гарри?
Я наконец добрался до прочтения этого произведения и с первых страниц меня не отпускает впечатление что магия Хогвардса и процесс компиляции шаблонов в C++ находятся где-то на одном слое мироздания. Как и в магии, если неправильно сформулировать «заклинание», результат может быть совершенно иным, чем ожидалось. Буквально на днях, кто-то из партнеров запустил нам парочку Огров в подвал, третий день отловить не можем, хоть весь подвал за эти дни отреверчивай.