Почему мой вариант возвращает значение, не равное назначенному? - PullRequest
1 голос
/ 13 марта 2020

Я пытаюсь создать простой вариант в качестве учебного упражнения.

Я хочу сделать это без динамического выделения памяти, как указано в спецификации c ++ для std::variant.

Для упрощения мой вариант может принимать только два значения.

Вот моя реализация:

//variant style class for two types
template<typename T1, typename T2>
class Either {
    using Bigest = std::conditional<sizeof(T1) >= sizeof(T2), T1, T2>;
    using ByteArray = std::array<std::byte, sizeof(Bigest)>;

    ByteArray val;
    std::optional<std::type_index> containedType;

public:

    Either() : containedType(std::nullopt) {} 

    template<typename T>
    Either(const T& actualVal) : containedType(typeid(T)) {         //ToDo check T is one of correct types
        ByteArray* ptr = (ByteArray*)&actualVal;
        val = *ptr;
    }

    class BadVariantAccess {};

    template<typename T>
    inline T& getAs() const {
        if(containedType == typeid(T)) {
            T* ptr = (T*)val.data();
            return *ptr;
        }
        else throw BadVariantAccess();
    }
};

Однако, когда я проверяю это, я получаю неправильное число после попытки получить значение:

int main() {

    Either<int,float> e = 5;

    std::cout << e.getAs<int>() << std::endl;

    return 0;
}

Возвращает случайное число (например, 272469509).

В чем проблема с моей реализацией и как ее исправить?

Ответы [ 2 ]

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

Ваша программа демонстрирует неопределенное поведение по разным причинам, поэтому немного бессмысленно пытаться объяснить, почему вы наблюдаете данное поведение. Но вот некоторые серьезные проблемы с вашим кодом:

  1. Не псевдоним любой памяти как std::array<std::byte, N>. Это является нарушением правила строгого алиасинга, единственными исключениями которого являются указатели на char и std::byte. При выполнении ByteArray* ptr = (ByteArray*)&actualVal; val = *ptr; вы вызываете std::array's конструктор копирования и передаете несуществующий экземпляр. В этот момент буквально все может произойти. Вместо этого вам следует либо копировать байты по одному (для тривиальных типов), либо использовать размещение new для копирования-конструирования объекта в хранилище на основе байтов.

  2. Ваш хранилище не выровнено, и это может вызвать сбои или серьезные потери производительности во время выполнения, в зависимости от вашей целевой платформы. Я не сразу уверен, может ли это привести к неопределенному поведению, но вам, безусловно, следует решить эту проблему, если вы хотите продолжить такое низкоуровневое управление памятью.

  3. Ваш конструктор копирования не проверяет фактический размер в памяти назначаемого типа. Если на какой-либо платформе у вас есть sizeof(int) > sizeof(float), то при копировании-конструировании Either<int, float> из float вы будете читать байты за концом числа с плавающей запятой и легко вызывать неопределенное поведение. Вы должны принять во внимание размер присваиваемого типа

  4. Если вы планируете хранить что-либо, кроме тривиальных типов (например, std::string или std::vector, а не только примитивы), вы ' Вам нужно будет позвонить в соответствующие операторы копирования / перемещения конструктора / присваивания. Для конструкторов (по умолчанию, перемещение, копирование) вам необходимо использовать размещение new для создания живых объектов в заранее выделенном хранилище. Кроме того, вам нужно использовать стирание типа, чтобы сохранить некоторую функцию, которая уничтожит содержащийся объект как правильный тип. Лямбда внутри std::function<void(std::byte*)> может быть очень полезна здесь, которая просто вызывает деструктор: [](std::byte* data){ (*reinterpret_cast<T*>(data)).~T(); } Это должно быть назначено каждый раз, когда вы сохраняете новый тип. Обратите внимание, что подобная ситуация является почти единственным случаем, когда вы когда-либо захотите вызвать деструктор вручную.

Я настоятельно призываю вас внимательно прочитать о том, как делать такие низкие Уровень управления памятью правильно. Это действительно легко сделать неправильно и не иметь ни малейшего представления, пока вы не столкнетесь с причудливыми ошибками намного позже.

Как любезно отмечено @MilesBudnek, using Biggest = std::conditional<sizeof(T1) >= sizeof(T2), T1, T2> даст тип trait , и поэтому экземпляр Biggest фактически будет экземпляром специализации структуры std::conditional и не T1 или T2. Вы, вероятно, имели в виду std::conditional_t<...> или std::conditional<...>::type; Скорее всего, ваш класс Either будет когда-либо выделять только один байт, что, очевидно, неверно.

1 голос
/ 13 марта 2020
    ByteArray* ptr = (ByteArray*)&actualVal;

Это бессмысленно. Вы говорите, что ptr указывает на std::array, но это не так. Конечно, вы получите мусор, если будете разыменовывать его.

...