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

Пару лет назад я написал статью про получение имен элементов 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++ находятся где-то на одном слое мироздания. Как и в магии, если неправильно сформулировать «заклинание», результат может быть совершенно иным, чем ожидалось. Буквально на днях, кто-то из партнеров запустил нам парочку Огров в подвал, третий день отловить не можем, хоть весь подвал за эти дни отреверчивай.

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

Комментарии (4)


  1. babypowder
    29.08.2024 22:46

    может проблема что:

    арахнофобы, вопреки распространённому мнению, «практически не осознают иррационального механизма собственных страхов»


  1. qw1
    29.08.2024 22:46

    То есть, в каждый экземпляр advisor_ratings_window мы положили поле bstring128 - строку с именем типа.
    Выглядит тяжеловато...

    Если делать чисто в compile-time, то поля в классе не будет и при итерации по экземплярам по имени типа не добраться
    https://godbolt.org/z/oGd9fh1Ko

    Но затраты на имя типа в экземпляре можно свести к минимуму, чисто до одного указателя
    https://godbolt.org/z/YfT6a8bs3

    Суффиксы-префиксы видимо так не отрежешь, придётся делать это в ран-тайме, уже при использовании имени.


    1. dalerank Автор
      29.08.2024 22:46

      пока наметки, надо подумать про совсем constexpr решение, тогда и строки эти будут гдето в статик памяти лежать.


    1. qw1
      29.08.2024 22:46

      Я ещё подумал, зачем отдельно advisor_window и advisor_window_t
      Оказалось, оправдано. Чтобы итерироваться по коллекции, нужен базовый нешаблонный класс.
      https://godbolt.org/z/d9chnPrhj