Как связать строку символов с функцией, создающей объект определёного класса?

Задача заключается в том, чтобы в зависимости от значения строки символов создавать объект определённого типа, например для строки "Class_A" создаётся объект типа A, для строки "Class_B" объект типа B и т. д. Все создаваемые объекты имеют общего предка, например класс Base. Причём необходимо, чтобы при добавлении нового типа объектов изменения в коде затрагивали только процесс создания этого нового объекта.

Пока есть только вариант с map, с ключом в виде строки и содержимым в виде указателя на функцию, возвращающую указатель на Base, но фактически в ней создаётся объект требуемого типа. При добавлении нового типа объекта требуется добавлять его в глобальную карту типов.

Возможно есть более простые решения?

class Base {
public:
virtual ~Base() {}
};

class A: public Base {
};

class B: public Base {
};

class C: public Base {
};

Base* f(std::string const& class_name);

auto a = f("Class_A"); // Создаёт объект класса A
auto b = f("Class_B"); // Создаёт объект класса B
auto c = f("Class_C"); // Создаёт объект класса C

Ответы (2 шт):

Автор решения: HolyBlackCat

Пока есть только вариант с map, с ключом в виде строки и содержимым в виде указателя на функцию, возвращающую указатель на Base ... При добавлении нового типа объекта требуется добавлять его в глобальную карту типов.

Возможно есть более простые решения?

Да. Та же самая map-а, но с автоматической регистрацией через CRTP.


Во-первых, нужно научиться получать имена типов. Тут есть два подхода: во время компиляции можно доставать имя из __FUNCSIG__/__PRETTY_FUNCTION__ (на MSVC и остальных компиляторах соответственно), либо в рантайме через typeid(...).name() (везде кроме MSVC результат нужно обработать (demangle) через __cxa_demangle).

Вот вариант, которым я пользуюсь: (первый способ, с причесыванием получившихся имен)

#include <algorithm>
#include <array>
#include <cstddef>
#include <string_view>

namespace type_name_details
{
    template <typename T>
    constexpr std::string_view RawTypeName()
    {
        #ifdef _MSC_VER
        return __FUNCSIG__;
        #else
        return __PRETTY_FUNCTION__;
        #endif
    }

    // This is only valid for `T = int`. Using a template to hopefully prevent redundant calculations.
    template <typename T = int>
    constexpr std::size_t prefix_len = RawTypeName<T>().rfind("int");

    // This is only valid for `T = int`. Using a template to hopefully prevent redundant calculations.
    template <typename T = int>
    constexpr std::size_t suffix_len = RawTypeName<T>().size() - prefix_len<T> - 3;

    // On MSVC, removes `class` and other unnecessary strings from type names.
    // Returns the new length.
    // It's recommended to include the null terminator in `size`, then we also null-terminate the resulting string and include it in the resulting length.
    constexpr std::size_t CleanUpTypeName(char *buffer, std::size_t size)
    {
        #ifndef _MSC_VER
        (void)buffer;
        return size;
        #else
        std::string_view view(buffer, size); // Yes, with the null at the end.

        auto RemoveTypePrefix = [&](std::string_view to_remove)
        {
            std::size_t region_start = 0;
            std::size_t source_pos = std::size_t(-1);
            std::size_t target_pos = 0;
            while (true)
            {
                source_pos = view.find(to_remove, source_pos + 1);
                if (source_pos == std::string_view::npos)
                    break;
                char ch = 0;
                if (source_pos == 0 || !(ch = view[source_pos - 1], (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '_'))
                {
                    std::size_t n = source_pos - region_start;
                    std::copy_n(view.begin() + region_start, n, buffer + target_pos);
                    target_pos += n;
                    source_pos += to_remove.size();
                    region_start = source_pos;
                }
            }
            std::size_t n = view.size() - region_start;
            std::copy_n(view.begin() + region_start, n, buffer + target_pos);
            target_pos += n;
            view = std::string_view(view.data(), target_pos);
        };

        RemoveTypePrefix("struct ");
        RemoveTypePrefix("class ");
        RemoveTypePrefix("union ");
        RemoveTypePrefix("enum ");

        { // Condense `> >` into `>>`.
            std::size_t target_pos = 1;
            for (std::size_t i = 1; i + 1 < view.size(); i++)
            {
                if (buffer[i] == ' ' && buffer[i-1] == '>' && buffer[i+1] == '>')
                    continue;
                buffer[target_pos++] = buffer[i];
            }
            if (!view.empty())
                buffer[target_pos++] = buffer[view.size()-1];
            view = std::string_view(view.data(), target_pos);
        }

        return view.size();
        #endif
    }

    template <std::size_t N>
    struct BufferAndLen
    {
        std::array<char, N> buffer;
        std::size_t len = 0;
    };

    template <typename T>
    constexpr auto storage = []{
        #ifndef _MSC_VER
        // On GCC and Clang, return the name as is.
        constexpr auto raw_name = RawTypeName<T>();
        std::array<char, raw_name.size() - prefix_len<> - suffix_len<> + 1> ret{};
        std::copy_n(raw_name.begin() + prefix_len<>, ret.size() - 1, ret.begin());
        return ret;
        #else
        // On MSVC, strip `class ` and some other junk strings.
        constexpr auto trimmed_name = []{
            constexpr auto raw_name = RawTypeName<T>();
            BufferAndLen<raw_name.size() - prefix_len<> - suffix_len<> + 1> ret{};
            std::copy_n(raw_name.begin() + prefix_len<>, ret.buffer.size() - 1, ret.buffer.begin());

            ret.len = CleanUpTypeName(ret.buffer.data(), ret.buffer.size());
            return ret;
        }();

        std::array<char, trimmed_name.len> ret{};
        std::copy_n(trimmed_name.buffer.begin(), trimmed_name.len, ret.begin());
        return ret;
        #endif
    }();
}

// Returns the type name (using `__PRETTY_FUNCTION__` or `__FUNCSIG__`, depending on the compiler).
template <typename T>
[[nodiscard]] constexpr std::string_view TypeName()
{
    return std::string_view(type_name_details::storage<T>.data(), type_name_details::storage<T>.size() - 1);
}

Например, TypeName<int>() == "int".


Теперь, используя это и CRTP может сделать автоматическую регистрацию:

class SimpleBase
{
  public:
    virtual ~SimpleBase() = default;

    virtual void Meow() = 0;
};

using ClassRegistry = std::map<std::string_view, std::unique_ptr<SimpleBase>(*)()>;

ClassRegistry &GetClassRegistry()
{
    // Должен быть внутри функции, чтобы инициализировался при первом вызове и не вызывал static initialization order fiasco.
    static ClassRegistry ret;
    return ret;
}

template <typename T>
class Base : public SimpleBase
{
    static std::nullptr_t RegisterType()
    {
        GetClassRegistry().try_emplace(TypeName<T>(), []() -> std::unique_ptr<SimpleBase> {return std::make_unique<T>();});
        return nullptr;
    }

    // Регистрирурем класс в мапу.
    inline static const std::nullptr_t register_type = RegisterType();
    // Принудительно инстанцируем `register_type`.
    static constexpr std::integral_constant<const std::nullptr_t *, &register_type> register_type_2{};
};

Пользоваться так:

class A : public Base<A> {void Meow() override {std::cout << "I'm A!\n";}};
class B : public Base<B> {void Meow() override {std::cout << "I'm B!\n";}};
class C : public Base<C> {void Meow() override {std::cout << "I'm C!\n";}};

int main()
{
    std::unique_ptr<SimpleBase> x = GetClassRegistry().at("B")();
    x->Meow(); // I'm B!
}

Полный код.


Если хочется кастомные имена типов вместо их автоматического определения, передавайте строку с именем типа в шаблонный параметр Base (вот так).

→ Ссылка
Автор решения: user7860670

Вот вариант попроще: вместо строк предлагаю использовать енумератор и делать дочерние классы использующие этот енумератор в качестве параметра. Это позволит обеспечить единственность места с перечнем классов - объявление енумератора с переносом ошибок при несоответствии реализаций классов на этап компиляции с этапа выполнения; создание экземпляра класса будет использовать индексирование вместо поиска по map; отсутствие завязки на стадию данамической инициализации; работающее автодополнение в редакторе при вызове функции (чего не будет для строки с именем класса, в которых можно запросто ошибиться и потом ловить исключения при выполнении). А если есть потребность именно в использовании строки в качестве идентификатора, то для преобразования строки в енумератор имеет смысл воспользоваться существующим инструментом (например boost.Describe).

class Base
{
    public: virtual ~Base() = default;
};

enum class
t_ClassId
{
    a
,   b
,   c

,   _size_
};

// Фабричный метод.
[[nodiscard]] ::std::unique_ptr<Base>
Spawn(t_ClassId const x_id);

// Реализация.

template<t_ClassId x_id>
class Derived;

template<>
class Derived<t_ClassId::a>
: public Base
{
    public: ~Derived() override = default;
};

template<>
class Derived<t_ClassId::b>
: public Base
{
    public: ~Derived() override = default;
};

template<>
class Derived<t_ClassId::c>
: public Base
{
    public: ~Derived() override = default;
};

#include <array>
#include <memory>
#include <utility>
#include <cstddef>

template<t_ClassId x_id>
[[nodiscard]] auto
Spawn()
{
    return ::std::unique_ptr<Base>(new Derived<x_id>{});
}

template<::std::size_t... x_id_pack>
[[nodiscard]] constexpr auto
Make_Lut([[maybe_unused]] ::std::index_sequence<x_id_pack...> dummy)
{
    return ::std::array{&Spawn<static_cast<t_ClassId>(x_id_pack)>...};
}

constexpr auto const lut
{
    Make_Lut(::std::make_index_sequence<static_cast<::std::size_t>(t_ClassId::_size_)>())
};

[[nodiscard]] ::std::unique_ptr<Base>
Spawn(t_ClassId const x_id)
{
    return (*lut[static_cast<::std::size_t>(x_id)])();
}

int main()
{
    auto p_a{Spawn(t_ClassId::a)};
    return 0;
}

online compiler

→ Ссылка