Как отмечали другие в комментариях к вашему вопросу, в общем случае невозможно обнаружить все существующие производные классы определенного базового класса во время компиляции с C ++.
Но если все, что вам нужно, это механизм, позволяющий избежать единого места, зная обо всех существующих производных классах, вы можете кое-что сделать, хотя и не во время компиляции.
Основная идея
Идея состоит в том, чтобы использовать инициализацию статических переменных-членов (что гарантированно произойдет до выполнения main
) для регистрации производных классов в общем реестре.
Такой реестр может выглядеть так:
class derived_registry
{
public:
static std::size_t number_of_instances()
{
return _instances.size();
}
static base* instance(std::size_t const index)
{
assert(index < _instances.size());
return _instances[index].get();
}
template <typename T, std::enable_if_t<std::is_base_of_v<base, T>, int> = 0>
static std::size_t register_derived_class(std::unique_ptr<T> instance)
{
auto const index = _instances.size();
_instances.emplace_back(std::move(instance));
return index;
}
private:
static std::vector<std::unique_ptr<base>> _instances;
};
inline std::vector<std::unique_ptr<base>> derived_registry::_instances;
Любой производный класс base
теперь должен зарегистрироваться, вызвав derived_registry::register_derived_class
для инициализации статической переменной-члена, например, вот так:
// in derived1.h
class derived1 : public base
{
public:
derived1();
void do_something() override;
private:
static std::size_t _index;
};
// in derived1.cpp
std::size_t derived1::_index = derived_registry::register_derived_class(std::make_unique<derived1>());
derived1::derived1()
: base{}
{
}
void derived1::do_something()
{
std::cout << "derived 1\n";
}
Это также показывает, почему важно определить вектор derived_registry::_instances
как переменную inline
: мы должны быть уверены, что derived_registry::register_derived_class
вызывается только после того, как derived_registry::_instances
уже инициализирован. Самый простой способ сделать это - использовать тот факт, что при наличии более одной переменной со статической продолжительностью хранения, определенной в одной единице перевода, они гарантированно будут инициализированы в том порядке, в котором они были определены. Поскольку мы определили derived_registry::_instances
в заголовочном файле, мы гарантируем, что derived_registry::_instances
инициализируется до derived1::_index
, и, следовательно, до вызова derived_registry::register_derived_class
.
Вы можете увидеть этот подход в действии на wandbox .
делает его надежным
Хотя приведенная выше реализация работает, она довольно громоздка, и все же есть вероятность, что кто-то добавит новый производный класс, но забудет его зарегистрировать.
Чтобы упростить регистрацию, вы можете использовать шаблон CRTP, как описано в ответе на этот вопрос , который StoryTeller связан в комментарии к вашему вопросу.
Хотя это может значительно упростить регистрацию, реестр по-прежнему работает корректно только тогда, когда каждый производный класс либо наследует базу CRTP, либо реализует саму регистрацию, которую легко забыть. Чтобы гарантировать, что нет другого выбора, кроме как наследовать от базового класса CRTP, вы можете дополнительно сделать конструктор base
закрытым и сделать базовый класс CRTP единственным другом. Тогда нельзя случайно наследовать напрямую от base
без регистрации нового производного класса.