Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to avoid undefined behaviour with aligned_storage and polymorphism

I have some code that basically do this:

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;

I highly doubt it's well defined behaviour. If it's indeed undefined behaviour, how can I fix it? In the code that do the reinterpret_cast, I only know the type of the base class.

On the other hand, if it's well defined behavior in all cases, why is this working and how?

Just keeping a reference to the contained object is not applicable here. In my code I want to apply SBO on a type erased list where the type is created by the user of my library, and basically extends the Base class.

I add elements inside a template function, but in the function that reads it, I cannot know the Derived type. The whole reason why I use a base class is because I only need the forward function in my code that reads it.

Here's what my code looks like:

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();
}
like image 672
Guillaume Racicot Avatar asked May 29 '17 03:05

Guillaume Racicot


3 Answers

Not sure about the exact semantics of whether reinterpret_cast is required to work with base class types, but you can always do this,

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;

Also why use the auto&& with the base reference in your code?


If you only know the type of the base class in your code then consider using a simple trait in an abstraction for the aligned_storage

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

and then you can now use the storage object to get the type it represents

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();
}

If you want this to work with perfect forwarding, then use a simple forwarding trait

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();
}

If you are using type erasure to create your derived class types which you then add to a homogenous container, you could do something like this

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();
like image 148
Curious Avatar answered Oct 23 '22 01:10

Curious


You may simply do

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

So no reinterpret_cast.

like image 41
Jarod42 Avatar answered Oct 23 '22 01:10

Jarod42


In the 'simple' exmaple you have, since you are casting from derived to base, either static_cast or dynamic_cast will work.

The more complex use case will end in tears, because the underlying values of a base pointer and a derived pointer to the same object need not be equal. It might work today, but fail tomorrow:

  1. reinterpret_cast does not play well with inheritance, especially multiple inheritance. If you ever to inherit from multiple basesand the first base class has size (or not have size if empty base optimization is not performed), reinterpret_cast to the second base class from an unrelated type will not apply the offset.
  2. Overloading does not play well with overriding. Templated classes should not have virtual methods. Templated classes with virtual methods should not be used with too much type deduction.
  3. The undefined behavior is fundamental to the manner in which MI is specified in C++, and unavoidable because you are trying to obtain something (in compile time) that you deliberated erased (in compile time). Just drop every virtual keyword from that class and implement everything with templates and everything will be more simple and correct.
  4. Are you sure your derived class objects can fit within 16 bytes? You probably need some static_assert.
  5. If you are willing to stomach the performance penalty introduced by virtual functions, why care about alignment?
like image 1
KevinZ Avatar answered Oct 22 '22 23:10

KevinZ