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