Как избежать неопределенного поведения сalign_storage и полиморфизмом

У меня есть код, который в основном делает это:

struct Base {
    virtual ~Base() = default;
    virtual int forward() = 0;
};

struct Derived : Base {
    int forward() override {
        return 42;
    }
};

typename std::aligned_storage<sizeof(Derived), alignof(Derived)>::type storage;

new (&storage) Derived{};
auto&& base = *reinterpret_cast<Base*>(&storage);

std::cout << base.forward() << std::endl;

Я очень сомневаюсь, что это четко определенное поведение. Если это действительно неопределенное поведение, как я могу это исправить? В коде, выполняющем reinterpret_cast, я знаю только тип базового класса.

С другой стороны, если это четко определенное поведение во всех случаях, почему это работает и как?

Простое сохранение ссылки на содержащийся объект здесь неприменимо. В своем коде я хочу применить SBO к стираемому списку типов, где тип создается пользователем моей библиотеки и в основном расширяет класс Base.

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

Вот как выглядит мой код:

union Storage {
    // not used in this example, but it is in my code
    void* pointer;

    template<typename T>
    Storage(T t) noexcept : storage{} {
        new (&storage) T{std::move(t)}
    }

    // This will be the only active member for this example
    std::aligned_storage<16, 8> storage = {};
}; 

template<typename Data>
struct Base {
    virtual Data forward();
};

template<typename Data, typename T>
struct Derived : Base<Data> {
    Derived(T inst) noexcept : instance{std::move(inst)} {}

    Data forward() override {
        return instance.forward();
    }

    T instance;
};

template<typename> type_id(){}
using type_id_t = void(*)();

std::unordered_map<type_id_t, Storage> superList;

template<typename T>
void addToList(T type) {
    using Data = decltype(type.forward());

    superList.emplace(type_id<Data>, Derived<Data, T>{std::move(type)});
}

template<typename Data>
auto getForwardResult() -> Data {
    auto it = superList.find(type_id<Data>);
    if (it != superList.end()) {
        // I expect the cast to be valid... how to do it?
        return reinterpret_cast<Base<Data>*>(it->second.storage)->forward();
    }

    return {};
}

// These two function are in very distant parts of code.
void insert() {
    struct A { int forward() { return 1; } };
    struct B { float forward() { return 1.f; } };
    struct C { const char* forward() { return "hello"; } };

   addToList(A{});
   addToList(B{});
   addToList(C{});
}

void print() {
   std::cout << getForwardResult<int>() << std::endl;
   std::cout << getForwardResult<float>() << std::endl;
   std::cout << getForwardResult<const char*>() << std::endl;
}

int main() {
   insert();
   print();
}

person Guillaume Racicot    schedule 29.05.2017    source источник
comment
@JamesRoot Если это действительно хорошо определено, покажите мне какую-нибудь ссылку, подтверждающую это или какую-то причину, по которой это должно работать.   -  person Guillaume Racicot    schedule 29.05.2017
comment
Я могу предложить вообще не использовать aligned_storage. Просто укажите пользовательский operator new, возвращающий выровненный фрагмент памяти и соответствующий operator delete для базового класса.   -  person Andrei R.    schedule 29.05.2017
comment
@АндрейР. Хотел бы я это сделать, но я пытаюсь применить SBO к коллекции, которая раньше содержала void*. Использование нового не будет СБО.   -  person Guillaume Racicot    schedule 29.05.2017
comment
Вы хотите использовать std::launder в С++ 17. Очень похоже/обман: small-object-storage-strict -aliasing-rule-and-undefined-behavior. См. Ответ экатмура и ответ Якка.   -  person WhiZTiM    schedule 29.05.2017
comment
Почему бы просто не сделать auto* derived = new (&storage) Derived{}; Base* base = derived; ?   -  person Jarod42    schedule 29.05.2017
comment
@WhiZTiM, к сожалению, с этим я застрял на C++ 11. Есть ли другой способ сделать это?   -  person Guillaume Racicot    schedule 29.05.2017
comment
@GuillaumeRacicot, не могли бы вы привести пример того, что на самом деле делает ваша функция шаблона?   -  person Curious    schedule 29.05.2017
comment
@ Любопытно, я обновил ответ.   -  person Guillaume Racicot    schedule 30.05.2017
comment
@GuillaumeRacicot, почему бы не удалить зависимость от структур A, B и C?   -  person Curious    schedule 30.05.2017
comment
@GuillaumeRacicot, если вы удалите их, вы можете сразу преобразовать их в производный тип без необходимости повышать базовый тип через reinterpret_cast   -  person Curious    schedule 30.05.2017
comment
@GuillaumeRacicot или, если вы хотите сохранить их, возможно, вам следует подумать о расширении функции getForwardResult(), чтобы она также принимала эти типы, и тогда вы можете reinterpret_cast к правильному производному типу таким образом   -  person Curious    schedule 30.05.2017
comment
@ Любопытно, что это классы, предоставленные пользователем моей библиотеки, а функция forward выполняет вычисления, определенные пользователем.   -  person Guillaume Racicot    schedule 30.05.2017
comment
Но никогда не может быть двух структур этих типов, которые возвращают один и тот же тип, верно? например, вы не можете создать другую структуру D, в которой есть функция переадресации, возвращающая int, потому что тогда unordered_map уже будет иметь запись для идентификатора типа int. Так что, по сути, это отношения 1-1, а это означает, что дополнительный уровень косвенности не требуется.   -  person Curious    schedule 30.05.2017
comment
@ Любопытно, как я уже говорил несколько раз, я не могу использовать эти типы в функции печати. Классы, которые пользователь добавляет в список, зависят от времени выполнения, и неизвестно, какой тип вставляется в список.   -  person Guillaume Racicot    schedule 30.05.2017
comment
@GuillaumeRacicot Также есть ли улучшение производительности при использовании трюка с указателем функции, а не std::type_index? Я никогда не пробовал это, и мне интересно :)   -  person Curious    schedule 30.05.2017
comment
@GuillaumeRacicot, но опять же, у вас не может быть нескольких типов, у которых есть прямые функции, возвращающие одно и то же, так зачем беспокоиться о них для начала? Вы можете просто иметь класс, созданный по шаблону int, double и тому, что вам нужно, и сделать его параметром шаблона класса шаблона Derived.   -  person Curious    schedule 30.05.2017
comment
@Curious Это работает хорошо, но трюк с адресом нестабилен для компилятора, из-за чего программа компилируется и компонуется, но не работает должным образом. Я мог бы изменить его, чтобы время компиляции хэшировало имя типа.   -  person Guillaume Racicot    schedule 30.05.2017
comment
@Curious Может быть класс D, который возвращает int. Использование A или D определяется во время выполнения. Для записи type_id может быть неопределенное количество существующих типов, но только один выбирается условием времени выполнения.   -  person Guillaume Racicot    schedule 30.05.2017
comment
@GuillaumeRacicot, скажем, условие выполнения, которое выполняет эту проверку, возвращает идентификатор типа структуры, которая должна использоваться для пересылки, что вы могли бы сделать, это вернуть указатель на функцию (или, что предпочтительнее, указатель базового класса, который указывает на производный тип с виртуальной таблицей) из этой функции, которая reinterpret_casts этот тип к правильному производному типу, а затем возвращает этот указатель как указатель Base<Data>. По сути, вы будете делать то же самое, что и я для примера стирания текста внизу, но с другим уровнем косвенности.   -  person Curious    schedule 30.05.2017


Ответы (3)


Не уверен в точной семантике того, требуется ли reinterpret_cast для работы с типами базового класса, но вы всегда можете это сделать,

typename std::aligned_storage<sizeof(Derived), alignof(Derived)>::type storage;

auto derived_ptr = new (&storage) Derived{};
auto base_ptr = static_cast<Base*>(derived_ptr);

std::cout << base_ptr->forward() << std::endl;

Также зачем использовать auto&& со ссылкой base в вашем коде?


Если вы знаете только тип базового класса в своем коде, рассмотрите возможность использования простого трейта в абстракции для aligned_storage

template <typename Type>
struct TypeAwareAlignedStorage {
    using value_type = Type;
    using type = std::aligned_storage_t<sizeof(Type), alignof(Type)>;
};

и теперь вы можете использовать объект хранилища, чтобы получить тип, который он представляет

template <typename StorageType>
void cast_to_base(StorageType& storage) {
    using DerivedType = std::decay_t<StorageType>::value_type;
    auto& derived_ref = *(reinterpret_cast<DerivedType*>(&storage));
    Base& base_ref = derived_ref;

    base_ref.forward();
}

Если вы хотите, чтобы это работало с идеальной пересылкой, используйте простую черту пересылки

namespace detail {
    template <typename TypeToMatch, typename Type>
    struct MatchReferenceImpl;
    template <typename TypeToMatch, typename Type>
    struct MatchReferenceImpl<TypeToMatch&, Type> {
        using type = Type&;
    };
    template <typename TypeToMatch, typename Type>
    struct MatchReferenceImpl<const TypeToMatch&, Type> {
        using type = const Type&;
    };
    template <typename TypeToMatch, typename Type>
    struct MatchReferenceImpl<TypeToMatch&&, Type> {
        using type = Type&&;
    };
    template <typename TypeToMatch, typename Type>
    struct MatchReferenceImpl<const TypeToMatch&&, Type> {
        using type = const Type&&;
    };
}

template <typename TypeToMatch, typename Type>
struct MatchReference {
    using type = typename detail::MatchReferenceImpl<TypeToMatch, Type>::type;
};

template <typename StorageType>
void cast_to_base(StorageType&& storage) {
    using DerivedType = std::decay_t<StorageType>::value_type;
    auto& derived_ref = *(reinterpret_cast<DerivedType*>(&storage));
    typename MatchReference<StorageType&&, Base>::type base_ref = derived_ref;

    std::forward<decltype(base_ref)>(base_ref).forward();
}

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

struct Base {
public:
    virtual ~Base() = default;
    virtual int forward() = 0;
};

/**
 * An abstract base mixin that forces definition of a type erasure utility
 */
template <typename Base>
struct GetBasePtr {
public:
    Base* get_base_ptr() = 0;
};

template <DerivedType>
class DerivedWrapper : public GetBasePtr<Base> {
public:
    // assert that the derived type is actually a derived type
    static_assert(std::is_base_of<Base, std::decay_t<DerivedType>>::value, "");

    // forward the instance to the internal storage
    template <typename T>
    DerivedWrapper(T&& storage_in)  { 
        new (&this->storage) DerivedType{std::forward<T>(storage_in)};
    }

    Base* get_base_ptr() override {
        return reinterpret_cast<DerivedType*>(&this->storage);
    }

private:
    std::aligned_storage_t<sizeof(DerivedType), alignof(DerivedType)> storage;
};

// the homogenous container, global for explanation purposes
std::unordered_map<IdType, std::unique_ptr<GetBasePtr<Base>>> homogenous_container;

template <typename DerivedType>
void add_to_homogenous_collection(IdType id, DerivedType&& object) {
    using ToBeErased = DerivedWrapper<std::decay_t<DerivedType>>;
    auto ptr = std::unique_ptr<GetBasePtr<Base>>{
        std::make_unique<ToBeErased>(std::forward<DerivedType>(object))};
    homogenous_container.insert(std::make_pair(id, std::move(ptr)));
}

// and then
homogenous_container[id]->get_base_ptr()->forward();
person Curious    schedule 29.05.2017
comment
Как я писал в вопросе, код, выполняющий reinterpret_cast, в моем случае не знает тип базового класса. - person Guillaume Racicot; 29.05.2017
comment
Это может работать, если у вас есть только один тип производного класса или нет полиморфной коллекции этих выровненных хранилищ. Однако кому-то может быть полезно, что это действительно так. - person Guillaume Racicot; 29.05.2017
comment
@GuillaumeRacicot, не могли бы вы привести пример того, как устроен ваш код? Я не понимаю, что вы имеете в виду, говоря, что у вас есть коллекция этих выровненных экземпляров хранилища. Вы стираете тип в однородном контейнере aligned_storageish? Это должно работать до тех пор, пока выровненные экземпляры хранилища не хранятся в каком-либо стертом контейнере. - person Curious; 29.05.2017
comment
Да. У меня есть коллекция выровненных хранилищ многих определяемых пользователем типов, которые расширяют базовый класс. Я не могу знать производный тип, потому что он определяется пользователем моей библиотеки. Простой using не вариант, потому что я не могу знать производный тип в месте, где находится reinterpret_cast. - person Guillaume Racicot; 29.05.2017
comment
@GuillaumeRacicot Каков тип вашей коллекции выровненных экземпляров хранилища? - person Curious; 29.05.2017
comment
Давайте продолжим обсуждение в чате. - person Guillaume Racicot; 29.05.2017

Вы можете просто сделать

auto* derived = new (&storage) Derived{};
Base* base = derived;

So no reinterpret_cast.

person Jarod42    schedule 29.05.2017
comment
Это не сработало бы для меня. Мне нужно держать их в виде списка стертых полиморфных объектов. - person Guillaume Racicot; 29.05.2017
comment
Я бы не стал использовать выровненное хранилище вашего решения. Как я уже сказал, я хочу применить SBO к элементам стираемого списка. Вы все еще можете увидеть мою диаграмму с Любопытным и другими комментариями. - person Guillaume Racicot; 29.05.2017

В «простом» примере, который у вас есть, поскольку вы выполняете преобразование из производного в базовое, будет работать либо static_cast, либо dynamic_cast.

Более сложный вариант использования закончится плачевно, потому что базовые значения базового указателя и производного указателя на один и тот же объект могут не совпадать. Это может работать сегодня, но не работать завтра:

  1. reinterpret_cast плохо работает с наследованием, особенно с множественным наследованием. Если вы когда-нибудь наследуете от нескольких баз, а первый базовый класс имеет размер (или не имеет размера, если пустая базовая оптимизация не выполняется), reinterpret_cast ко второму базовому классу из несвязанного типа не будет применяться смещение.
  2. Перегрузка плохо сочетается с переопределением. Шаблонные классы не должны иметь виртуальных методов. Шаблонные классы с виртуальными методами не следует использовать со слишком большим выводом типов.
  3. Неопределенное поведение лежит в основе способа, которым MI задается в C++, и неизбежно, потому что вы пытаетесь получить что-то (во время компиляции), что вы преднамеренно стерли (во время компиляции). Просто отбросьте каждое ключевое слово virtual из этого класса и реализуйте все с помощью шаблонов, и все будет проще и правильнее.
  4. Вы уверены, что объекты вашего производного класса могут уместиться в пределах 16 байт? Вам, наверное, нужно немного static_assert.
  5. Если вы готовы мириться с потерей производительности, вызванной виртуальными функциями, зачем заботиться о выравнивании?
person KevinZ    schedule 30.05.2017