Is cast from pointer to one aligned struct to another with same "prefix" members legal?

Viewed 233

I tried to create a smart pointer that has only one pointer to a block of memory, which starts with a reference counter (control block), and a value stored immediately after it. And after reading something from the forums, the standard and cppreference, I realized that it looks like the code is full of UB.

  1. So, is this code legit relative to the C++17 standard?
#include <iostream>

#include <memory>
#include <type_traits>
#include <new>

struct alignas(alignof(size_t)) StorageBase {
    size_t m_rc = 0;
};

template<typename T>
struct Storage: public StorageBase {
    using value_type = T;

    std::aligned_storage_t<sizeof(value_type), alignof(value_type)> m_value_storage;
};

class Dummy {
public:
    Dummy() { std:: cout << "Dummy constructed" << std::endl; }
    virtual ~Dummy() { std:: cout << "Dummy destructed" << std::endl; }
};

class DerivedDummy: public Dummy {
public:
    DerivedDummy() { std:: cout << "DerivedDummy constructed" << std::endl; }

    ~DerivedDummy() override { std:: cout << "DerivedDummy destructed" << std::endl; }
};

int main(int argc, char const *argv[]) {
    using first_storage_type = Storage<DerivedDummy>;

    auto* first_storage = new first_storage_type;
    ++first_storage->m_rc;
    new (&first_storage->m_value_storage) first_storage_type::value_type();

    StorageBase* storage = first_storage;

    using second_storage_type = Storage<Dummy>;

    if constexpr (std::is_convertible_v<first_storage_type::value_type*, second_storage_type::value_type*>) {
        using second_value_type = second_storage_type::value_type;

        // UB ?
        auto* second_storage = static_cast<second_storage_type*>(storage);

        ++second_storage->m_rc;

        // UB ?
        std::launder<second_value_type>(
            static_cast<second_value_type*>(
                static_cast<void*>(
                    &second_storage->m_value_storage
                )
            )
        )->~second_value_type();

        delete second_storage;
    }

    return 0;
}

Because I know there exist solutions, where you add pointer to StorageBase class to aligned_storage<...>::type and it will work fine.

  1. But, is there any solutions without overhead, that uses only alignment and will conform to the standard?

Second attempt

After reading more info about alignment and standard layout I refactored code to new one.

  1. Is it legit?
#include <iostream>

#include <memory>
#include <type_traits>
#include <new>

// explicit alignment here...
struct alignas(size_t) StorageBase {
    size_t m_rc = 0;
};

template<typename T>
// ... and here
struct alignas(StorageBase) Storage: public StorageBase {
    using value_type = T;

    std::aligned_storage_t<sizeof(value_type), alignof(value_type)> m_value_storage;
};

class Dummy {
public:
    Dummy() { std:: cout << "Dummy constructed" << std::endl; }
    virtual ~Dummy() { std:: cout << "Dummy destructed" << std::endl; }
};

class DerivedDummy: public Dummy {
public:
    DerivedDummy() { std:: cout << "DerivedDummy constructed" << std::endl; }

    ~DerivedDummy() override { std:: cout << "DerivedDummy destructed" << std::endl; }
};

int main(int argc, char const *argv[]) {
    using first_storage_type = Storage<DerivedDummy>;

    auto* first_storage = new first_storage_type;
    ++first_storage->m_rc;

    const auto ptr_to_storage = new (&first_storage->m_value_storage) first_storage_type::value_type();

    StorageBase* storage = first_storage;

    using second_storage_type = Storage<Dummy>;

    if constexpr (std::is_convertible_v<first_storage_type::value_type*, second_storage_type::value_type*>) {
        using second_value_type = second_storage_type::value_type;

        /*
         * Storage<T> is explicitly aligned as StorageBase,
         * but Storage<T> is not standard layout
        */
        second_value_type* ptr = reinterpret_cast<second_value_type*>(storage + 1); // UB?

        ++storage->m_rc;

        std::launder<second_value_type>(ptr)->~second_value_type();

        ::operator delete(storage);
    }

    return 0;
}
0 Answers
Related