Объединиение или наследование C++

313
02 апреля 2017, 05:06

Всем привет, разрабатываю язык программирования на c++ (надо для учёбы), но в плюсах я пока новичок, и отсюда возникают различные вопросы из раздела "а как лучше?"

Собственно, в чём проблема: нужен менеджер динамической памяти (грубо говоря, это штука, которая какому-то имени присваивает какие-то данные). Тип данных может меняться (это может быть, например, массив из чаров или инстанс какого-то класса — не важно). К текущему моменту я использовал объединения (union), в которых я просто перечислял возможные варианты типов.

    union JMemoryDataPointer {
        int* intData;
        float* floatData;
        char* stringData;
        bool* boolData;
    } data;

Но недавно подумал, что можно сделать какой-то класс, например, Data и потом наплодить ему детей DataInteger, DataThing и т.д.

Ну, и собственно, вопрос: какой вариант лучше и почему? (хотя бы по вашему мнению)

Answer 1

У вашего метода есть несколько серьёзных изъянов:

  1. Необходимость нисходящего приведения от базового класса к дочернему (в случае @KoVadim-а) или небезопасного обращения к полям объединения (в вашем случае). При этом можно легко опечататься и случайно преобразовать не к тому типу с плачевным результатом.

  2. Отсутствие контроля обработки всех возможных типов в перечислении. Особо остро данная проблема встанет при добавлении поддержки нового элемента перечисления в изрядно разросшуюся программу.

  3. Отсутствие возможности хранения чего-либо, отличного от примитивных типов данных. Пока это несущественно, но в будущем может возникнуть необходимость инкапсуляции некой логики в тип о типе данных вашего языка программирования. Примером такой логики может служить проверка значений параметров при их изменении. Из-за этого поля придётся делать закрытыми, выставляя вместо них методы. Следом за методами появится и конструктор, который нельзя будет вызвать из union-а.

Для решения первой проблемы был придуман шаблон проектирования «Посетитель» («Visitor»). Его суть заключается в том, что информация о текущем типе хранимого значения известна только контейнеру (union) и наружу не передаётся. Как же тогда работать с хранимой переменной, спросите вы. Очень просто. При необходимости доступа к переменной мы передаём контейнеру совокупность обработчиков, каждый из которых предназначен для работы со своим определённым типом. Контейнер же сам выбирает из них нужный и вызывает его с преобразованным в соответствующий тип значением.

Удобной реализацией этого шаблона проектирования является класс boost::variant. Для обработки хранимого типа у него имеется функция boost::apply_visitor(), принимающая экземпляр некоего класса с перегрузками метода T_out operator()(T_in). Да-да, обработчики могут ещё и возвращать значения по результатам работы с переменной. В придачу этот метод может быть ещё и шаблонным, покрывая сразу несколько (или все) типов за раз.

Наконец, этот класс решает две оставшиеся проблемы ещё на этапе компиляции.

Рассмотрим его использование на примере:

#include <boost/variant/variant.hpp>
// Вспомогательный класс владеющего указателя (урезанный аналог std::unique_ptr из C++11)
template<class T> class MyUniqueArrayPtr
{
    T* _ptr;
public:
    explicit MyUniqueArrayPtr(size_t count) : _ptr(new[] T(count)) {}
    ~MyUniqueArrayPtr() {delete[] _ptr;}
    T* operator*() const {return _ptr};
    T* operator->() const {return _ptr};
};
// Контейнер информации о переменных
typedef variable_t boost::variant<
    MyUniqueArrayPtr<int>,
    MyUniqueArrayPtr<float>,
    MyUniqueArrayPtr<char>,
    MyUniqueArrayPtr<bool>
>;
// Класс для получения строкового представления значения переменной (для редактора кода
// или отладчика)
class StringifyingVisitor : public boost::static_visitor<std::string>
{
public:
    // Шаблонный метод, преобразующий в строку всё, поддерживаемое std::to_string()
    template<class T>
    std::string operator()(T val) const
    {
        // Доступна, начиная с C++11; приведена просто для примера
        return std::to_string(*val);
    }
};
// Класс для компиляции машинного кода
class CompilationVisitor : public boost::static_visitor<>
{
public:
    // Пусть в конструкторе будет передаваться общий тип действия для переменной,
    // чтобы компилятор смог наставить машинной команде нужных типозависимых флагов.
    explicit CompilationVisitor(Command command)
    {
        // ...
    }
    // С boost::variant можно принимать и константные ссылки. А можно и неконстантные.
    void operator()(const MyUniqueArrayPtr<int>& val)
    {
        // ...
    }
    void operator()(const MyUniqueArrayPtr<float>& val)
    {
        // ...
    }
    // ...
};
// Тестовая функция, принимающая откуда-то информацию о переменной
void foo(const variable_t& variable)
{
    // Методы первого посетителя шаблонные, поэтому его можно создать прямо по месту
    // вызова
    std::cout << boost::apply_visitor(StringifyingVisitor(), variable) << std::endl;
    // Второй посетитель
    CompilationVisitor compiler(someCommand);
    boost::apply_visitor(compiler, variable);
}
Answer 2

Когда-то мне что-то подобное нужно было, а использовать шаблоны было нельзя в итоге сделал такую вот штуковину: github Смотрите файлы datatilepropertys.h и datatilepropertys.cpp там есть методы as_int(), as_string() и т.д.

READ ALSO
Количество вхождений паттерна в строке C++

Количество вхождений паттерна в строке C++

Привет всем! Как посчитать количество вхождений паттерна regex в сроке? Язык С++

211
Не работает метод write.string(*str) в rapidjson, как решить?

Не работает метод write.string(*str) в rapidjson, как решить?

На вход в json передаю vector<string>vecs, но выдает ошибку(коммит в конце), помогите решить

270
Как прочитать большой файл в QByteArray

Как прочитать большой файл в QByteArray

Не читается большой файл в QByteArray (182111787 byte):

327
Инициализация вектора в конструкторе

Инициализация вектора в конструкторе

Почему вектор инициализируется рандомным значением? Константа ещё не успевает стать -1? Как тогда сделать правильно?

262