Данную проблему можно решить с помощью паттерна Visitor. Ниже приведен пример реализации данного паттерна. Он громоздкий, но сложного в нем ничего нет.
В начале нужно обратить внимание на класс MyVariant. Так как C++ имеет строгую типизацию, то мы не можем просто так записать любой объект в любую переменную, как в языках с динамической типизацией, поэтому вначале нам нужно избавится от типа. В языке C для этих целей обычно используется void* и в этом примере мы будем использовать именно его. При получении создании объекта MyVariant будет создан указатель на int или string, а затем тип будет приведен к void* и записан в переменную var_. Но проблема возникнет, когда тип нам все-таки понадобится, поэтому предварительно сохраним информацию о нем в отдельную переменную type_. В общем, в конструкторах ничего интересного нет: мы объявили конструктор для int, string и конструктор перемещения. Удалять void* напрямую нельзя, поэтому в деструкторе мы приводим указатель к нужному типу перед удалением (static_cast).
В main создается массив объектов типа MyVariant, но работать с ними просто так мы не можем, так как у нас больше нет информации о типах и нам нужно ее предварительно восстановить. Для этого объявлен интерфейс VisitorInterface и его реализация в виде PrinterVisitor, которая выводит значение в терминал. В main создается объект PrinterVisitor и передается в метод Visit объекта MyVariant, который, имея информацию о типа в переменной type_, приводит void* к int* или string*, а потом вызывает соответствующий метод из VisitorInterface.
#include <string>
#include <vector>
#include <iostream>
class VisitorInterface
{
public:
virtual void Dispatch(int) = 0;
virtual void Dispatch(const std::string&) = 0;
};
class PrinterVisitor : public VisitorInterface
{
public:
void Dispatch(int i) {std::cout << i << std::endl;}
void Dispatch(const std::string& s) {std::cout << s << std::endl;}
};
class MyVariant
{
private:
enum class Type
{
None,
Int,
String
};
void* var_ = nullptr;
Type type_ = Type::None; ///< Type of var_ variable
public:
explicit MyVariant(int val)
{
var_ = new int(val);
type_ = Type::Int;
}
explicit MyVariant(const std::string& val)
{
var_ = new std::string(val);
type_ = Type::String;
}
MyVariant(MyVariant&& other)
{
var_ = other.var_;
type_ = other.type_;
other.var_ = nullptr;
other.type_ = Type::None;
}
~MyVariant()
{
switch (type_)
{
case Type::Int:
delete static_cast<int*>(var_);
break;
case Type::String:
delete static_cast<std::string*>(var_);
break;
}
}
void Visit(VisitorInterface& visitor)
{
switch (type_)
{
case Type::Int:
visitor.Dispatch(*static_cast<int*>(var_));
break;
case Type::String:
visitor.Dispatch(*static_cast<std::string*>(var_));
break;
}
}
};
int main(int /*argc*/, char** /*argv*/)
{
std::vector<MyVariant> vec;
vec.emplace_back(5);
vec.emplace_back("Hello habr!");
vec.emplace_back(10);
PrinterVisitor printer;
for (auto& el : vec)
{
el.Visit(printer);
}
return 0;
}
Данное решение будет работать, наверное, в любой версии C++, но в C++17 появилась более удобная альтернатива: std::variant + std::visit.
В данном примере мы создаем массив значений типа std::variant. Для доступа к значению конкретного элемента затем используется std::visit, который автоматически сделает все, что было сделано в первой реализации (сгеренирует перегруженные методы и switch блоки). Для реализации различной логики в зависимости от типа мы используем if времени компиляции.
#include <string>
#include <vector>
#include <iostream>
#include <variant>
int main(int /*argc*/, char** /*argv*/)
{
std::vector<std::variant<int, std::string>> vec;
vec.emplace_back(5);
vec.emplace_back("Hello habr!");
vec.emplace_back(10);
for (auto& el : vec)
{
std::visit([](auto& arg){
using T = std::decay_t<decltype(arg)>;
if constexpr(std::is_same_v<T, int>)
{
std::cout << "int: " << arg << std::endl;
}
else if constexpr(std::is_same_v<T, std::string>)
{
std::cout << "string: " << arg << std::endl;
}
}, el);
}
return 0;
}