У вас есть несколько вариантов.Сначала я объясню свое предпочтительное решение.
1.Используйте динамическую диспетчеризацию
Если у вас есть массив типа базового класса, почему вы вообще хотите что-то делать с Var
?Эта переменная специфична для дочернего класса.Если у вас где-то есть A
, вас не должно волновать, что B
имеет или не имеет в этом месте.
Операции над типизированной переменной должны быть заключены в виртуальную функцию в базовом классе.Если вы хотите выполнить условие и прочее, возможно, вы могли бы инкапсулировать это условие в виртуальную функцию, которая возвращает логическое значение.
2a.Отбросьте базовый класс и используйте вариант
Иногда вы заранее знаете количество типов, которые попадут в этот список.Использование варианта и отбрасывание базового класса - хорошее решение, которое может применяться к вашему делу.
Допустим, у вас есть только int
, double
и std::string
:
using poly = std::variant<B<int>, B<double>, B<std::string>>;
std::array<poly, 3> arr;
arr[0] = B<int>{};
arr[1] = B<double>{};
arr[2] = B<std::string>{};
// arr[2] = B<widget>{}; // error, not in the variant type
std::visit(
[](auto& b) {
using T = std::decay_t<decltype(b)>;
if constexpr (std::is_same_v<B<int>, T>) {
b.Var = 2; // yay!
}
},
arr[0]
);
2b.Отбросьте базовый класс и используйте универсальные функции
Полностью удалите базовый класс и задайте шаблоны для своих функций, которые над ними работают.Вы можете переместить все свои функции в интерфейс или множество std::function
.Управляйте этим вместо прямой функции.
Вот пример того, что я имел в виду:
template<typename T>
void useA(T const& a) {
a.Var = 34; // Yay, direct access!
}
struct B {
std::function<void()> useA;
};
void createBWithInt() {
A<int> a;
B b;
b.useA = [a]{
useA(a);
};
};
Это хорошо для случаев, когда у вас всего несколько операций.Но это может быстро привести к раздуванию кода, если у вас много операций или если у вас много типов std::function
.
3.Использовать посетителя
Вы можете создать посетителя, который будет отправлять сообщения нужного типа.
Это решение будет очень близко к тому, что вы исключаете, но довольно сложное и может легко сломаться при добавлении дел.
Примерно так:
struct B_Details {
protected:
struct Visitor {
virtual accept(int) = 0;
virtual void accept(double) = 0;
virtual void accept(std::string) = 0;
virtual void accept(some_type) = 0;
};
template<typename T>
struct VisitorImpl : T, Visitor {
void accept(int value) override {
T::operator()(value);
}
void accept(double) override {
T::operator()(value);
}
void accept(std::string) override {
T::operator()(value);
}
void accept(some_type) override {
T::operator()(value);
}
};
};
template<typename T>
struct B : private B_Details {
template<typename F>
void visit(F f) {
dispatch_visitor(VisitorImpl<F>{f});
}
private:
virtual void dispatch_visitor(Visitor const&) = 0;
};
// later
B* b = ...;
b->visit([](auto const& Var) {
// Var is the right type here
});
Затем, конечно, вам нужно реализовать dispatch_visitor
для каждого дочернего класса.
4.Используйте std::any
Это буквально возвращает переменную с типом стирания.Вы не можете выполнить какие-либо операции с ним, не приведя его обратно:
class A {
std::any GetVar()
};
Мне лично не нравится это решение, потому что оно может легко сломаться и не является универсальным.Я бы даже не использовал полиморфизм в этом случае.