I think you gave the answer yourself in your various observations:
- You want raw memory and placement new. This requires to have at least one byte available, even if you want to construct an empty object via placement new.
- You want zero bytes overhead for storing any empty objects.
These requirements are self-contradicting. The answer therefore is No, that is not possible.
You could change your requirements a bit more, though, by requiring the zero byte overhead only for empty, trivial types.
You could define a new class trait, e.g.
template <typename T>
struct constructor_and_destructor_are_empty : std::false_type
{
};
Then you specialize
template <typename T, typename = void>
class raw_container;
template <typename T>
class raw_container<
T,
std::enable_if_t<
std::is_empty<T>::value and
std::is_trivial<T>::value>>
{
public:
T& data() noexcept
{
return reinterpret_cast<T&>(*this);
}
void construct()
{
// do nothing
}
void destruct()
{
// do nothing
}
};
template <typename T>
struct list_node : public raw_container<T>
{
std::atomic<list_node*> next_;
};
Then use it like this:
using node = list_node<empty<char>>;
static_assert(sizeof(node) == sizeof(std::atomic<node*>), "Good");
Of course, you still have
struct bar : raw_container<empty<char>> { empty<char> e; };
static_assert(sizeof(bar) == 1, "Yes, two objects sharing an address");
But that is normal for EBO:
struct ebo1 : empty<char>, empty<usigned char> {};
static_assert(sizeof(ebo1) == 1, "Two object in one place");
struct ebo2 : empty<char> { char c; };
static_assert(sizeof(ebo2) == 1, "Two object in one place");
But as long as you always use construct and destruct and no placement new on &data(), you’re golden.