Гарри Поттер и имя типа в компайлтайм

Пытаюсь понять почему код не компилится

Пытаюсь понять почему код не компилится

Пару лет назад я написал статью про получение имен элементов enum в моих любимых плюсах без использования typeid, макросов и черной магии, а то и вообще в компайлтайм. Хотя нет, немного магии там все же было. Это был интересный опыт, но особого применения в проде я так и нашел, хотя коллеги начали активно использовать эту возможность чтобы итерироваться по enum в поисках нужного элемента по его строковому представлению. Оно конечно задумывалось наоборот, но как говорится, пасту в тюбик обратно не запихнешь, пользуются и то радость. И тут в домашнем игровом движке мне понадобился похожий функционал получения имени структуры или класс в компайлтайм, можно конечно было сделать через typeid, но в релизной сборке я планируют отключать rtti, так что этот вариант не подходит. А конвертировать имя структуры в строку все же хочется. При чем тут Гарри и для чего это все нужно в конце статьи.

И так как в домашнем проекте нет жестких рамком неиспользования стандартной библиотеки, и возможности готовых контейнеров вроде array/string_view сильно упростили код и вообще структуру всего решения. Получение имени типа в C++ та еще головная боль, казалось бы что известно компилятору на этапе сборки проекта должно легко доставаться какой-нибудь встроенной функцией вроде __builtin_typename, но этого как вы понимаете нет, и в грядущих стандартах тоже не предвится. Ближайший способ получить имя типа — это использовать std::type_info::name, который на этапе компиляции не существует, потому что еще не собрана таблица типов, с которой эта структура работает. Да и вообще type_info не гарантирует, что результат будет читаемым для человека.

Если вам интересно как я добрался до __PRETTY_FUNCTION__, об этом подробно рассписано в прошлой статье. Не буду повторяться, начну с того места, что возможность получить человеко-читаемую информацию в относительно любом месте кода есть. Если обернуть нужный тип в шаблонную функцию, добавив внутрь секретный вызов и сохранить куда-то результат, то имя типа прекрасно видно.

Программисты балуются с шаблонами

Программисты балуются с шаблонами

template 
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() << std::endl;
    std::cout << "Type name: " << type_name_to_str() << std::endl;
    std::cout << "Type name: " << type_name_to_str() << std::endl;
}

Поиграться можно тут (godbolt), выхлоп кланга нам пока не интересен, главное что он тоже позволяет провернуть такой трюк. Отпиливание префикса и постфикса не слишком сложная задача, поэтому весь код приведен сразу. Отдельно напомню, то приведенная логика работает как с оптимизациями, так и без, это важно потому что компилятор мог запросто выкинуть ненужную информацию вроде той, что возвращает __PRETTY_FUNCTION__ в релизе, заменим имя типа первым попавшимся набором символов.

x86-64 gcc (trunk)
Program returned: 0
Program stdout
Type name: int; std::string_view = std::basic_string_view
Type name: double; std::string_view = std::basic_string_view
Type name: std::__cxx11::basic_string; std::string_view = std::basic_string_view


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, который умеет инициализироваться в компайлтайм. Но просто передать туда строку тоже не получится, потому что инициализация std: array происходит поэлементно, а вывод __PRETTY_FUNCTION__ это const char * по факту. В строке, даже constexpr, можно обращаться к отдельным элементам по индексу. Если воспользоваться этим свойством, то можно разбить строку на отдельные символы, и далее полученную последоваться отправить в конструктор std: array.

Итак давайте соберем все вместе, и чтобы это все работало без простыни кода, воспользуемся умением компилятора автоматически выводить типы аргументов шаблона. А индексы сгенерируем через std::make_index_sequence, где N это длина изначальной строки. Массив здесь выступает в роли промежуточного хранилища, в конце которого идет терминальный символ, я не нашел с сожалению более красивого способа сформировать строку.

std::index_sequence - здесь будут лежать индексы символов в строке
std::string_view - здесь будет лежать сами данных из __PRETTY_FUNCTION__ 
std::array - сюда через конструктор мы положим данные из строки поэлементно

template 
constexpr auto str_to_array(std::string_view str, std::index_sequence) {
  return std::array{ str[Idxs]..., '\0' };
}

Иногда компилируется полная фигня

Иногда компилируется полная фигня

Возвращаясь к самому первому примеру кода из статьи, можно его немного дописать, чтобы иметь возможность получать искомую строку в нормальном виде. (godbolt).

template 
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{});
}

int main() {
    std::cout << (char*)type_name_str().data() << std::endl;
}

Очевидный минус такого решения это неудобный синтаксис вызова, чтобы приблизить его к синтаксису стандартной библиотеки и помочь компилятору закешировать уже найденные типы надо добавить синтаксического сахара и привести к более привычному виду (godbolt)

template 
struct type_name_holder {
  static inline constexpr auto value = type_name_str();
};

template 
constexpr std::string_view type_name() {
  constexpr auto& value = type_name_holder::value;
  return std::string_view{value.data(), value.size()};
}

int main() {
    std::cout << type_name() << std::endl;
}

А зачем вообще это надо?

45550e30ed60a916f513b8271a5c3b02.png

В свободное время я восстанавливаю игру и движок старенького ситибилдера 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
struct advisor_window_t : public advisor_window {
    inline advisor_window_t() : advisor_window(type_name().data()) {
...

struct advisor_ratings_window : public advisor_window_t {
    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 лет продолжить работу над проектом.

852e7ab9f394af04d0d5d29b6e8518bc.png

При чем тут Гарри?

Я наконец добрался до прочтения этого произведения и с первых страниц меня не отпускает впечатление что магия Хогвардса и процесс компиляции шаблонов в C++ находятся где-то на одном слое мироздания. Как и в магии, если неправильно сформулировать «заклинание», результат может быть совершенно иным, чем ожидалось. Буквально на днях, кто-то из партнеров запустил нам парочку Огров в подвал, третий день отловить не можем, хоть весь подвал за эти дни отреверчивай.

Скомпилилось!

Скомпилилось!

© Habrahabr.ru