9

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();
}
Guillaume Racicot
  • 39,621
  • 9
  • 77
  • 141
  • @JamesRoot If it's indeed well defined, please show me some reference that comfirm this or some reason why it should work. – Guillaume Racicot May 29 '17 at 04:04
  • I can suggest not using `aligned_storage` at all. Just provide custom `operator new`, returning aligned memory chunk and corresponding `operator delete` for base class. – Andrei R. May 29 '17 at 04:39
  • @AndreiR. I wish I could do that, but I'm trying to apply SBO to a collection that held `void*` before. Using new would not be SBO. – Guillaume Racicot May 29 '17 at 04:42
  • You want to use `std::launder` in C++17. Very similar/dupe: [small-object-storage-strict-aliasing-rule-and-undefined-behavior](https://stackoverflow.com/questions/39477443/small-object-stack-storage-strict-aliasing-rule-and-undefined-behavior). See both ecatmur's answer and Yakk's answer – WhiZTiM May 29 '17 at 10:24
  • Why not simply do `auto* derived = new (&storage) Derived{}; Base* base = derived;` ? – Jarod42 May 29 '17 at 11:24
  • @WhiZTiM sadly, I'm stuck with C++11 with this one. Is there another way to do that? – Guillaume Racicot May 29 '17 at 13:47
  • @GuillaumeRacicot could you provide an example of what your template function is actually doing? – Curious May 29 '17 at 17:23
  • @Curious I updated the answer. – Guillaume Racicot May 29 '17 at 21:38
  • @GuillaumeRacicot why not remove the dependency on the `A`, `B` and `C` structs? – Curious May 29 '17 at 21:54
  • @GuillaumeRacicot if you remove them you can straight away cast to the derived type without having to upcast to the base type via the `reinterpret_cast` – Curious May 29 '17 at 21:55
  • @GuillaumeRacicot or if you want to keep them maybe you should consider augmenting the `getForwardResult()` function to accept those types as well, and then you can `reinterpret_cast` to the right derived type that way – Curious May 29 '17 at 21:57
  • @Curious those are classes provided by the user of my library and the `forward` function does claculations defined by the user. – Guillaume Racicot May 29 '17 at 21:57
  • But there can never be two structs of those types that return the same type right? for example you cannot make another struct `D` that has a forward function returning an `int` because then the `unordered_map` will already have an entry for the `int` typeid. So essentially its a 1-1 relationship, meaning that the extra layer of indirection is not required – Curious May 29 '17 at 21:58
  • @Curious as I said multiple time, I cannot use those types in the print function. The classes the user is adding to the list depend at runtime, and what type is inserted into the list is unknown. – Guillaume Racicot May 29 '17 at 21:59
  • @GuillaumeRacicot Also is there a performance improvement to using the function pointer trick rather than `std::type_index`? I have never tried this and am curious :) – Curious May 29 '17 at 21:59
  • @GuillaumeRacicot but again, you cannot have multiple types that have forward functions returning the same thing, so why bother having them to begin with? You can just have a class that is templated on `int`, `double`, and whatever you need and make that the template parameter of the `Derived` template class – Curious May 29 '17 at 22:00
  • @Curious It works well, but the address trick is not stable across compiler, which makes the program to compile and link, but fails to run properly. I might change it to compile time hashing of the type name. – Guillaume Racicot May 29 '17 at 22:01
  • @Curious There can be a class `D` that returns an `int`. If `A` or `D` is used is defined at runtime. For an entry of type_id, there can be an indefinite number of existing types, but only one is chosen by a runtime condition. – Guillaume Racicot May 29 '17 at 22:03
  • @GuillaumeRacicot, say the runtime condition that does that check returns the typeid of the struct which is to be used to forward, what you could do is return a function pointer (or the preferable thing, a base class pointer, which points to a derived type with the virtual table) from that function as well which `reinterpret_cast`s that type to the right Derived type and then returns that pointer as a `Base` pointer. Essentially you would be doing the same thing I did for the type erasure example at the bottom but with a different layer of indirection. – Curious May 29 '17 at 23:16

3 Answers3

4

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();
Curious
  • 20,870
  • 8
  • 61
  • 146
  • As I wrote in the question, the code doing the reinterpret_cast don't know the type of the base class in my case. – Guillaume Racicot May 29 '17 at 03:57
  • This could work if you only have one type of Derived class, or no polymorphic collection of these aligned storage. However, it could be useful for someone that it's indeed the case. – Guillaume Racicot May 29 '17 at 04:10
  • @GuillaumeRacicot could you give me an example of how your code is structured? I don't follow what you mean by you having a collection of these aligned storage instances.. Do you erase the type in a homogenous `aligned_storage`ish container? This should work as long as the aligned storage instances are not stored in some type erased container – Curious May 29 '17 at 04:11
  • Yes. I have a collection of aligned storage, of many user defined type that extends the base class. I cannot know the derived type because it is defined by the user of my library. A simple `using` is not an option, because I cannot know the derived type at the location where the `reinterpret_cast` is. – Guillaume Racicot May 29 '17 at 04:14
  • @GuillaumeRacicot What is the type of your collection of aligned storage instances? – Curious May 29 '17 at 04:16
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/145333/discussion-between-guillaume-racicot-and-curious). – Guillaume Racicot May 29 '17 at 04:16
2

You may simply do

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

So no reinterpret_cast.

Jarod42
  • 203,559
  • 14
  • 181
  • 302
  • That wouldn't work for me. I need to hold them in type erased list of polymorphic object. – Guillaume Racicot May 29 '17 at 13:31
  • I wouldn't use aligned storage of your solution was an option. As I said, I want to apply SBO on the elements of a type erased list. You can still see my chart with Curious and other comments – Guillaume Racicot May 29 '17 at 13:45
1

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?
KevinZ
  • 3,036
  • 1
  • 18
  • 26