Как связать строку символов с функцией, создающей объект определёного класса?
Задача заключается в том, чтобы в зависимости от значения строки символов создавать объект определённого типа, например для строки "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 шт):
Пока есть только вариант с 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 *, ®ister_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
(вот так).
Вот вариант попроще: вместо строк предлагаю использовать енумератор и делать дочерние классы использующие этот енумератор в качестве параметра. Это позволит обеспечить единственность места с перечнем классов - объявление енумератора с переносом ошибок при несоответствии реализаций классов на этап компиляции с этапа выполнения; создание экземпляра класса будет использовать индексирование вместо поиска по 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;
}