У меня есть фабрика, содержащая карту с именами объектов в качестве ключа и указателя для функции значения.
template<class T>
static T* makeObj()
{
return new T();
}
using createFunction = std::function<void*()>;
map<std::string, createFunction> types;
template<class T>
static void add(const std::string& name)
{
types.insert(name, makeObj<T>);
}
Я хочу добавить дополнительную функцию для получения отфильтрованного списка типов (получить список унаследованных классов), примерно так:
template<class baseType>
static std::list<std::string> getFilteredListOfTypes()
{
}
Вопрос в том, как это сделать. Первая идея — создать карту со структурой в качестве значения и сохранить там объект соответствующего типа, я могу использовать Dynamic_cast и проверить все типы, но сомневаюсь, что создание объекта — хорошая идея. Другой способ — использовать std::is_base_of:
template< class Base, class Derived >
struct is_base_of;
is_base_of принимает два типа: класс Base я могу получить из моего getFilteredListOfTypes, но как получить производный класс? Создайте объект и используйте decltype... но здесь снова создается объект.
Есть ли способ создать такую функцию фильтра без создания объектов? Создавать здесь объект — плохая идея? Разве это плохая идея — создавать объект для каждого элемента карты? Меня немного пугает ситуация, когда на заводе будут сотни совсем не маленьких типов.
УПД AppFactory.h:
class AppFactory
{
public:
template<class T>
static T* makeObj()
{
return new T();
}
using createFunction = std::function<void*()>;
using registerMap = tsl::robin_map<std::string, createFunction>;
static registerMap& get();
template<class T>
static std::unique_ptr<T> createUnique(const std::string& name)
{
return std::unique_ptr<T>(create<T>(name));
}
template<class T>
static std::shared_ptr<T> createShared(const std::string& name)
{
return std::shared_ptr<T>(create<T>(name));
}
template<class T>
static T* create(const std::string& name)
{
if (get().contains(name))
{
return static_cast<T*>(get()[name]());
}
return nullptr;
}
template<class T>
static bool add(const std::string& name)
{
auto resultPair = get().insert_or_assign(name, makeObj<T>);
return resultPair.second;
}
};
#define APPFACTORY_ADD(classname) \
namespace { static bool addName = AppFactory::add<classname>(#classname); }
AppFactory.cpp:
AppFactory::registerMap& AppFactory::get()
{
static AppFactory::registerMap map;
return map;
}
Использование фабрики:
...
APPFACTORY_ADD(SomeClass);
...
AppFactory::createUnique<SomeBaseClass>("SomeClass")
...
УПД 2 тестовая реализация getFilteredListOfTypes
struct typedata
{
createFunction func;
Object* obj;
};
using registerMap = tsl::robin_map<std::string, typedata>;
...
template<class T>
static bool add(const std::string& name)
{
typedata data;
data.func = makeObj<T>;
data.obj = new T();
auto resultPair = get().insert_or_assign(name, data);
return resultPair.second;
}
...
template<class BaseType>
static std::list<std::string> getFilteredListOfTypes()
{
std::list<std::string> typeList;
for(auto it = get().begin(); it != get().end(); ++it)
{
if (dynamic_cast<BaseType*>(get()[it->first].obj))
{
typeList.push_back(it->first);
}
}
return typeList;
}
Использование getFilteredListOfTypes
class A : public Object
...
class B : public A
...
class C : public B
...
class D : public C
...
class E : public B
...
lst = AppFactory::getFilteredListOfTypes<A>(); // lst -> {"A", "B", "C", "D", "E"}
lst = AppFactory::getFilteredListOfTypes<B>(); // lst -> {"B", "C", "D", "E"}
lst = AppFactory::getFilteredListOfTypes<C>(); // lst -> {"C", "D"}
lst = AppFactory::getFilteredListOfTypes<D>(); // lst -> {"D"}
Основная идея — фильтровать список типов без объектов. Я хотел бы использовать is_base_of, но у меня есть один из необходимых типов для этой функции внутри функции «add» и другой необходимый тип внутри функции «getFilteredListOfTypes», и я не знаю, что я могу здесь сделать. Если я буду использовать созданные объекты для функции фильтра, то, конечно, я буду использовать динамический_cast
«Создать объект и использовать decltype» Это void *
, ничего толкового.
«makeObj» и «void*» будут использоваться клиентом. Я думаю, что могу сделать что-то вроде этого: template<class T> static void add(const std::string& name) { typeData data; data.createFunc = makeObj<T>; data.defaultObj = Т(); типы.вставка(имя, данные); }
Ваша фабрика ничего не знает о типах. Все, что у него есть, это map<string, void(*)()
. Отсюда невозможно восстановить какую-либо информацию о типе.
Не совсем понятно, чего вы хотите добиться. Ваша строка types.insert(name, makeObj<T>);
даже не компилируется. std::is_base_of
— это метапрограммирование шаблонов, оно работает только во время компиляции.
Добавьте, пожалуйста, пример использования на вашем заводе.
data.defaultObj = T();
Еще раз что за тип data.defaultObj
?
Я добавил весь код фабрики и его использование, чтобы прояснить вопрос.
Можете ли вы показать, как бы вы реализовали эту функцию, если бы вас не беспокоила стоимость создания объекта? decltype(T{})
не требует каких-либо затрат времени выполнения конструктора T
, поэтому я до сих пор не понимаю, что вы имеете в виду.
Другими словами, кажется, что вы формулируете свой вопрос как оптимизацию существующей реализации (которую вы не показали), в то время как (другие и) я подозреваю, что здесь есть какая-то более фундаментальная проблема.
@WeijunZhou Дело не в оптимизации. Вопрос в том, как получить список всех классов, производных от некоторого класса X
, который можно создать с помощью карты.
@Yksisarvinen Хорошо, наверное, я не совсем ясно выразился. На самом деле я имею в виду, что он спрашивает, как сделать что-то с некоторым ограничением, хотя неясно, как это можно сделать даже без ограничения, при этом ограничение «без создания объекта».
Судя по вашему редактированию, это работает только в том случае, если тип является производным от Object
, поэтому вы также можете объявить прототип функции возвращающим Object*
вместо void*
. Это важная дополнительная информация, потому что, как правило, с void*
мало что можно сделать, но Object*
— это другое.
Да, я изменил void* на Object* для этой тестовой реализации, насколько я понимаю, в любом случае void* нужно будет изменить.
На самом деле я ищу какой-нибудь хитрый способ использования is_base_of (я на 90% уверен, что это невозможно, но продолжайте искать). Какой-то способ «хранить» информацию о типе внутри функции «add» и использовать эту информацию внутри «getFilteredListOfTypes», тогда для is_base_of будет два типа.
В этом ответе предлагается написать собственную крошечную структуру «отражения во время выполнения». Будем надеяться, что однажды C++ будет иметь правильное отражение во время компиляции (или времени выполнения), и этот ответ устареет. До тех пор...
TL;DR: Живая демоверсия (обозреватель компилятора)
Нашей целью будет создание экземпляра нешаблонного класса TypeInfo
для каждого типа возвращаемого значения фабрики. У этого типа есть функция-член bool isBaseOf(TypeInfo const& other)
, которую можно использовать во время выполнения и вести себя как std::is_base_of
.
#include <span>
struct TypeTag{};
using TypeId = TypeTag const*;
// This is NOT a templated class.
class TypeInfo {
public:
using DirectBases = std::span<TypeInfo const* const>;
explicit constexpr TypeInfo(TypeId typeId, DirectBases directBases)
: typeId_{ typeId }
, directBases_{ directBases }
{}
constexpr bool isBaseOf(TypeInfo const& child) const {
if (*this == child) {
return true;
}
for (TypeInfo const* directBase : child.directBases_) {
if (isBaseOf(*directBase)) {
return true;
}
}
return false;
}
constexpr bool operator==(TypeInfo const& other) const {
return typeId_ == other.typeId_;
}
protected:
TypeId typeId_;
DirectBases directBases_;
};
И карта времени компиляции typename T
-> TypeInfo{...}
, действующая как база данных информации об отражениях:
template<typename T>
struct TypeInfoOf;
template<typename T>
constexpr TypeInfo const& typeInfoOf() {
return TypeInfoOf<T>::value;
}
При этом getFilteredListOfTypes
можно реализовать как:
#include <list>
#include <string>
#include <unordered_map>
// NOTE (digression): I see a singleton anti-pattern here...
class AppFactory {
public:
struct RegisterMapValue {
// CreateFunction func;
TypeInfo const* typeInfo;
};
using RegisterMap = std::unordered_map<std::string, RegisterMapValue>;
static RegisterMap& get() {
static RegisterMap map;
return map;
}
template<class T>
static void add(const std::string& name) {
get()[name] = { /*func,*/ &typeInfoOf<T>() };
}
static std::list<std::string> getFilteredListOfTypes(TypeInfo const& base) {
std::list<std::string> typeList;
for(auto const& mapPair : get()) {
if (base.isBaseOf(*mapPair.second.typeInfo)) {
typeList.push_back(mapPair.first);
}
}
return typeList;
}
template<class BaseType>
static std::list<std::string> getFilteredListOfTypes() {
return getFilteredListOfTypes(typeInfoOf<BaseType>());
}
};
Теперь, чтобы создать эти объекты TypeId, нам нужно сгенерировать две вещи.
TypeId
объект для каждого типа.TypeInfo*
, содержащий прямые основания объекта.template<typename T>
struct UniqueId {
static constexpr auto tag = TypeTag{};
};
template<typename T>
constexpr TypeId uniqueTypeIdOf() {
return &UniqueId<T>::tag;
}
static_assert(uniqueTypeIdOf<std::string>() == uniqueTypeIdOf<std::string>());
static_assert(uniqueTypeIdOf<unsigned>() != uniqueTypeIdOf<std::string>());
К сожалению, мы не можем автоматически получить список прямых баз типа. Мы просто создадим утилиту, которая принимает базовые типы в качестве входных данных, превращает их в объект std::array<TypeInfo*>
и создает объект TypeInfo
:
#include <array>
#include <type_traits>
template<typename T, typename... DirectBases>
struct TypeInfoMaker {
private:
static_assert((std::is_base_of_v<DirectBases,T> && ...));
static_assert((!std::is_same_v<T,DirectBases> && ...));
using BasesArray = std::array<TypeInfo const*, sizeof...(DirectBases)>;
static constexpr auto directBases = BasesArray{ {&typeInfoOf<DirectBases>()...} };
public:
static constexpr auto value = TypeInfo{ uniqueTypeIdOf<T>(), directBases };
};
Именно здесь проявляется главный недостаток этого ответа: список базового класса дублируется между объявлением класса и конструкцией TypeInfo
. Оба должны быть обновлены вместе вручную (если только не используются макросы, но это будет некрасиво).
2 static_assert
в TypeInfoMaker
обеспечивают некоторую защиту от ошибок: TypeInfoMaker
выдаст ошибку времени компиляции, если база неверна (но будет принимать косвенные базы), и должна предотвращать циклические ссылки (TypeInfo::isBaseOf завершится). Мы мало что можем сделать против пропавших баз.
// User-provided class (unchanged)
class A {
public:
virtual ~A() = default;
};
// The user has to declare A's reflection data as follows:
template<>
struct TypeInfoOf<A> : TypeInfoMaker<A> {};
class B : public A {};
template<>
struct TypeInfoOf<B> : TypeInfoMaker<B, A> {};
class C : public B {};
template<>
struct TypeInfoOf<C> : TypeInfoMaker<C, B> {};
class D : public C {};
template<>
struct TypeInfoOf<D> : TypeInfoMaker<D, C> {};
class E : public B {};
template<>
struct TypeInfoOf<E> : TypeInfoMaker<E, B> {};
С другой стороны, для заполнения карты TypeInfoOf
требуется всего пара строк каждого типа.
Объедините все предыдущие фрагменты кода и добавьте следующее:
#include <iostream>
#include <string_view>
template<typename T>
void printFilteredList(std::string_view typeName) {
std::cout << "filteredListOfTypes<" << typeName << ">: ";
for (auto const& value : AppFactory::getFilteredListOfTypes<T>()) {
std::cout << value << ' ';
}
std::cout << '\n';
}
int main() {
AppFactory::add<A>("A");
AppFactory::add<B>("B");
AppFactory::add<C>("C");
AppFactory::add<D>("D");
AppFactory::add<E>("E");
printFilteredList<A>("A");
printFilteredList<B>("B");
printFilteredList<C>("C");
printFilteredList<D>("D");
printFilteredList<E>("E");
}
Вы отказались от информации о типе, когда решили использовать
void*
. Теперь только объект времени выполнения знает свой собственный тип, и любое предоставленное решение на этом этапе будет в основном реализовывать отражение времени выполнения.