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.
- 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.
- 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.
- 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;
}
WM_NOTIFY
notifications, for example. Each notification'slParam
contains aNMHDR*
pointer, but various APIs sendingWM_NOTIFY
will actual point thelParam
to larger structs that contain aNMHDR
as their first member and extra data as additional members, and soWM_NOTIFY
handlers are expected to cast theNMHDR*
pointer according to the actual notification type so they can access the extra data as needed. – Southeasterlysockaddr
-based structs used in socket API calls that takesockaddr*
parameters, where allsockaddr_...
structs (sockaddr_in
,sockaddr_in6
,sockaddr_un
, etc) have a commonfamily
member in front to denote the actual struct type, so that asockaddr*
pointer to asockaddr_...
instance can be casted correctly to access its extra data fields. – Southeasterlystorage
assecond_storage_type
are both UB. Casting tovoid*
and then tosecond_value_type
is effectivelyreinterpret_cast
, and accessing through that pointer is also UB. Thelaunder
does nothing. This is assuming theDummy
types are supposed to be arbitrary. – Louisiana++second_storage->m_rc
, remove the rest. – Louisianafamily
is the first data member and those structs have standard layout. This makes pointers to sockaddr interconvertible with pointers toshort
. So it's legal to cast toshort*
and read it, then cast to the correct type. Same thing forNMHDR
and its friends. – SenescentNMHDR*
to egOBJECTPOSITIONS*
validates all of them. – Senescent