So I thought about this a little more and realized that, although you can't use the padding inside the stored objects, you can try to arrange Node
's members so they take up the least amount of space. Of course you could hard-code that, but I thought a more elegant way would be to automate it.
To find the smallest arrangement we need to generate all possible arrangements and pick the smallest one. Generating all arrangements can be accomplished with metafunction permutations
, which generates all permutations of a pack of types:
template <typename P1, typename P2>
struct merge {};
template <template <typename...> class P, typename... Ts, typename... Us>
struct merge<P<Ts...>, P<Us...>> {
using type = P<Ts..., Us...>;
};
template <typename T, typename P>
struct prepend {};
template <typename T, template <typename...> class P, typename... Packs>
struct prepend<T, P<Packs...>> {
using type = P<typename merge<P<T>, Packs>::type...>;
};
// N is the number of rotations to go
template <std::size_t N, typename Pack, typename = void>
struct permutations_impl {};
template <template <typename...> class P, typename... Ts>
struct permutations_impl<0, P<Ts...>> {
// All rotations done, break the recursion
using type = P<>;
};
template <std::size_t N, template <typename...> class P, typename T>
struct permutations_impl<N, P<T>> {
using type = P<P<T>>;
};
template <std::size_t N, template <typename...> class P, typename F, typename... Rest>
struct permutations_impl<N, P<F, Rest...>, std::enable_if_t<(sizeof...(Rest) && N != 0)>> {
using PermuteRest = typename permutations_impl<sizeof...(Rest), P<Rest...>>::type;
using NextRotation = typename permutations_impl<N-1, P<Rest..., F>>::type;
using type = typename merge<typename prepend<F, PermuteRest>::type, NextRotation>::type;
};
template <typename Pack>
struct permutations {};
template <template <typename...> class P, typename... Ts>
struct permutations<P<Ts...>> {
using type = typename permutations_impl<sizeof...(Ts), P<Ts...>>::type;
};
To pick the smallest of all permutations we can then do something like this:
template <typename Pack>
struct min_size_element {
static constexpr std::size_t min_index(std::initializer_list<std::size_t> l) {
std::size_t min = *l.begin();
std::size_t idx = 0, result = 0;
for(auto it = l.begin(); it != l.end(); ++it, ++idx) {
if(*it < min) min = *it, result = idx;
}
return result;
}
};
template <typename... Ts>
struct min_size_element<std::tuple<Ts...>> : min_size_element<void> {
using type = std::tuple_element_t<min_index({sizeof(Ts)...}), std::tuple<Ts...>>;
};
template <typename... Ts>
struct smallest_tuple {
using tuples = typename permutations<std::tuple<Ts...>>::type;
using type = typename min_size_element<tuples>::type;
};
The Node
class would then store the key, value, and used flag in a tuple chosen by smallest_tuple
. The elements need to be accessed by type (because we don't know their index), so key and value elements need to be wrapped in unique wrapper types. An implementation of the Node
class with wrappers and accessors for key and value could look like this:
template <typename K, typename V>
class Node {
union KeyWrap {
struct{} _;
K key;
constexpr KeyWrap() : _() {}
};
union ValueWrap {
struct{} _;
V value;
constexpr ValueWrap() : _() {}
};
// Need to wrap key and value types because tuple elements are accessed by type
using Storage = typename detail::smallest_tuple<bool, KeyWrap, ValueWrap>::type;
Storage mStorage;
public:
constexpr Node() = default;
~Node() {
if(this->used()) {
this->key().~K();
this->value().~V();
}
}
Node& operator=(std::pair<K, V> entry) {
if(this->used()) {
this->key() = std::move(entry.first);
this->value() = std::move(entry.second);
} else {
new(&std::get<KeyWrap>(mStorage).key) K(std::move(entry.first));
new(&std::get<ValueWrap>(mStorage).key) V(std::move(entry.second));
std::get<bool>(mStorage) = true;
}
return *this;
}
bool used() const {
return std::get<bool>(mStorage);
}
K& key() {
assert(this->used());
return std::get<KeyWrap>(mStorage).key;
}
V& value() {
assert(this->used());
return std::get<ValueWrap>(mStorage).value;
}
};
Live demo
I also tried to automatically detect whether a type has some padding, but wasn't able to do it reliably, and only for types which can be derived from. Have a look at my answer here.