std::variant¶
One of the big downsides of unions
is that you must explicitly invoke the destructor of its active member. Even if you define a destructor for the union
, it will not implicitly call the destructors
of the union’s member element. It would not know, of course, which element has been most recently set and in need of being destructed.
Below is a nested union that is used to hold to two types of pairs: one where pair<Key, Value>::first
is const; the other, in which pair<Key, Value>::first
is not const. To make it easier to use, it implements
a move constructor and move assignment constructor, as well as the usual constructors.
template<class Key, Value> class tree234 {
union KeyValue {
std::pair<Key, Value> _pair; // ...this pair eliminates constantly having to do: const_cast<Key>(p.first) = some_noconst_key;
std::pair<const Key, Value> _constkey_pair;
public:
KeyValue() {}
~KeyValue()
{
_pair.first.~Key(); // Note: Anonymous unions require explicit destructor calls.
_pair.second.~Value();
}
KeyValue(Key key, const Value& value) : _pair{key, value} {}
KeyValue(const KeyValue& lhs) : _pair{lhs._pair.first, lhs._pair.second} {}
KeyValue(Key k, Value&& v) : _pair{k, std::move(v)} {}
KeyValue(KeyValue&& lhs) : _pair{move(lhs._pair)} {}
KeyValue& operator=(const KeyValue& lhs) noexcept;
KeyValue& operator=(KeyValue&& lhs) noexcept;
constexpr Key& key() { return _pair.first; }
constexpr const Key& key() const { return _constkey_pair.first; }
constexpr Value& value() { return _pair.second; }
constexpr const Value& value() const { return _constkey_pair.second; }
constexpr const std::pair<Key, Value>& pair() const { return _pair; }
constexpr std::pair<Key, Value>& pair() { return _pair; }
constexpr const std::pair<const Key, Value>& constkey_pair() const { return _constkey_pair; }
constexpr std::pair<const Key, Value>& constkey_pair() { return _constkey_pair; }
friend std::ostream& operator<<(std::ostream& ostr, const KeyValue& key_value)
{
ostr << "{" << key_value._pair.first << ',' << key_value._pair.second << "}, ";
return ostr;
}
};
class Node { // Tree node elements. The root is of a managed type std::unique_ptr<Node>
// The Node's children are also of managed type std::unique_ptr<Node>
// Node contains define outside of tree23
friend class tree23<Key, Value>;
public:
Node(Key key, const Value& value, Node *ptr2parent=nullptr);
~Node()
{
}
// We disallow copy construction and assignment...
Node(const Node&) = delete;
Node& operator=(const Node&) = delete;
Node(Node&&); // ...but we allow move assignment and move construction.
Node& operator=(Node&&) noexcept;
// Constructor for just coping the keys and values.
Node(const std::array<KeyValue, 2>& lhs_keys_values, Node * const lhs_parent, int lhs_totalItems) noexcept;
constexpr Key& key(int i) { return keys_values[i].key(); }
constexpr const Key& key(int i) const { return keys_values[i].key(); }
constexpr Key& value(int i) { return keys_values[i].value(); }
constexpr const Key& value(int i) const { return keys_values[i].value(); }
constexpr std::pair<Key, Value>& pair(int i) { return keys_values[i].pair(); }
constexpr const std::pair<const Key, Value>& pair(int i) const { return keys_values[i].pair(); }
constexpr bool isLeaf() const noexcept { return (children[0] == nullptr && children[1] == nullptr) ? true : false; }
//...snip
private:
Node *parent;
std::array<KeyValue, 2> keys_values;
std::array<std::unique_ptr<Node>, 3> children;
//...snip
};
std::unique_ptr<Node> root; // root of tree
// snip....
}; // end tree234
When a tree234<std::string, std::string>
goes out of scope, root
is implicitly destructor, and thus std::unique_ptr<Node>
calls delete on the raw Node
pointer, which in turn results in calling the
destructor of each element of type KeyValue
in std::array<KeyValue, 3>
(as well as invoking the destructor for std::array<std::unique_ptr<Node>, 3>
, the children, which results in a cascade of recursive
calls, first to the immediate children, then grandchildren, and so on).
However, when KeyValue::~KeyValue()
is invoked, it does not invoke the destructor for the std::pair<Key, Value::first
or std::pair<Key, Value::second
, which is why the destructor explicitly must do so:
KeyValue::~KeyValue()
{
_pair.first.~Key(); // Note: Anonymous unions require explicit destructor calls.
_pair.second.~Value();
}
This is obviously error prone. std::variant
does not have this draw back. For a compelte description of std::variant
and how to use it see Everything You Need to Know About std::variant from C++17.