Правильный дизайн для класса C ++, охватывающий несколько возможных типов - PullRequest
0 голосов
/ 04 марта 2020

Я пытаюсь реализовать класс C ++, который будет переносить значение (среди прочего). Это значение может быть одного из нескольких типов (строка, буфер памяти, число, вектор).

Простой способ реализовать это - сделать что-то вроде этого

class A {
    Type type;

    // Only one of these will be valid data; which one will be indicated by `type` (an enum)
    std::wstring wData{};
    long dwData{};
    MemoryBuffer lpData{};
    std::vector<std::wstring> vData{};
};

This чувствует себя неэлегичным и как будто это тратит впустую память.

Я также попытался реализовать это как объединение, но это сопровождалось значительными накладными расходами на разработку (определение пользовательских деструкторов / конструкторов перемещения / конструкторов копирования), и даже со всеми этими, там все еще были ошибки, с которыми я столкнулся.

Я также рассмотрел вопрос о создании A базового класса и создании производного класса для каждого возможного значения, которое он может содержать. Также кажется, что это не лучший способ решить проблему.

Мой последний подход - сделать каждый член std::optional, но это все равно добавляет некоторые издержки.

Что подход будет лучшим? Или есть другой дизайн, который работает лучше, чем любой из них?

Ответы [ 2 ]

4 голосов
/ 04 марта 2020

Использование std::variant. Он безопасен, проверен и точно подходит для конечного числа возможных типов. Это также избавляет от типа enum.

class A {
    std::variant<std::wstring, long, MemoryBuffer, std::vector<std::wstring>> m_data{}; // default initializes the wstring.
public 
    template<class T>
    void set_data(T&& data) {
        m_data = std::forward<T>(data);
    }

    int get_index() { // returns index of type.
        m_data.index();
    }

    long& get_ldata() {
        return std::get<long>(m_data); // throws if long is not the active type
    }

    // and the others, or

    template<class T> 
    T& get_data() { // by type
        return std::get<T>(m_data);
    }

    template<int N>
    auto get_data() { // by index
        return std::get<N>(m_data);
    }
};

// using:
A a;
a.index() == 0; // true
a.set_data(42); 
a.index() == 1; // true
auto l = a.get<long>(); // l is now of type long, has value 42
a.get<long>() = 1;
l = a.get<1>();

PS: этот пример даже не включает самую крутую (на мой взгляд) особенность std::variant: std::visit Я не уверен, что вы хотите сделать со своим классом, поэтому я не могу создать содержательный пример. Если вы дадите мне знать, я подумаю об этом.

0 голосов
/ 04 марта 2020

Вы в основном хотите QVariant без остальной части Qt, тогда:)?

Как уже упоминали другие, вы можете использовать std::variant и поместить using MyVariant = std::variant<t1, t2, ...> в некоторый общий заголовок, а затем использовать его везде, где это требуется. Это не так уж и элегантно, как вы можете подумать - определенные типы c, которые нужно передать, предоставляются только в одном месте. Это единственный способ сделать это без создания механизма метатипа, который может инкапсулировать операции с любым типом объекта.

Вот где приходит boost::any: он делает именно это. Он оборачивает понятия и, таким образом, поддерживает любой объект, который реализует эти понятия. Какие концепции требуются, зависит от вас, но в целом вы должны выбрать достаточное их количество, чтобы сделать тип пригодным для использования и полезным, но не слишком много, чтобы преждевременно исключить некоторые типы. Вероятно, это путь к go, у вас будет: using MyVariant = any<construct, _a>; затем (где construct - список контрактов, пример которого приведен в качестве примера в документации и _a является заполнителем типа из boost::type_erasure.

Принципиальное различие между std::variant и boost::any заключается в том, что variant параметризован для конкретных типов , тогда как Параметр any параметризован для контрактов, с которыми связаны типы . Затем любой из них с радостью сохранит произвольный тип, который выполняет все эти контракты. "Центральное расположение", в котором вы определяете псевдоним для варианта типа будет постоянно расти с variant, так как вам нужно инкапсулировать больше типов. С any центральное расположение будет в основном stati c и будет меняться редко, так как изменение требований контракта, вероятно, потребует исправлений / адаптации к перевозимые типы, а также точки использования.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...