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

0

7

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;
}
Shin answered 8/10, 2020 at 19:3 Comment(11)
On Windows, the Win32 API depends on this very behavior for WM_NOTIFY notifications, for example. Each notification's lParam contains a NMHDR* pointer, but various APIs sending WM_NOTIFY will actual point the lParam to larger structs that contain a NMHDR as their first member and extra data as additional members, and so WM_NOTIFY handlers are expected to cast the NMHDR* pointer according to the actual notification type so they can access the extra data as needed.Southeasterly
Windows and other platforms also rely on this behavior for numerous sockaddr-based structs used in socket API calls that take sockaddr* parameters, where all sockaddr_... structs (sockaddr_in, sockaddr_in6, sockaddr_un, etc) have a common family member in front to denote the actual struct type, so that a sockaddr* pointer to a sockaddr_... instance can be casted correctly to access its extra data fields.Southeasterly
@RemyLebeau the APIs you mention are from C, not C++ though. C++ has strict aliasing, which I think would complicate this. The "common" members you mention is legal in C++ though -- but only through a union, not through a pointer AFAIKMagnetoelectricity
There's too much going on in your snippet. Accessing and deleting storage as second_storage_type are both UB. Casting to void* and then to second_value_type is effectively reinterpret_cast, and accessing through that pointer is also UB. The launder does nothing. This is assuming the Dummy types are supposed to be arbitrary.Louisiana
@Human-Compiler I'm aware of that. Though the APIs I mentioned are based in C, they require C++ to behave the same way too, legal or not.Southeasterly
If all you want to ask is ++second_storage->m_rc, remove the rest.Louisiana
@Human-Compiler C also has strict aliasing. Except that it is unclear what they mean by "access".Expression
@RemyLebeau for sockaddr family is the first data member and those structs have standard layout. This makes pointers to sockaddr interconvertible with pointers to short. So it's legal to cast to short* and read it, then cast to the correct type. Same thing for NMHDR and its friends.Senescent
@RemyLebeau don't forget that Microsoft makes a compiler too, so they can do dodgy things and still be assured they'll work correctly in their compiler.Alastair
@Senescent well, technically such casts are not legal under strict aliasing, but Microsoft ignored that. But other vendor's compilers (at least for Windows) have to make this work too to maintain compatibility with the API.Southeasterly
@RemyLebeau such casts are perfectly legal basic.compound 4.3. The conditions are very specific, but casting NMHDR* to eg OBJECTPOSITIONS* validates all of them.Senescent

© 2022 - 2024 — McMap. All rights reserved.