Вопрос: Как в массиве хранить указатели на объекты разных типов?

Лучший ответ: Если все эти типы унаследованны от одного и того же класса, то можно хранить указатели на базовый класс.

Худший ответ: Данную проблему можно решить с помощью паттерна 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;
}