Search code examples
c++language-lawyertype-punning

Type punning between `pair<Key, Value>` and `pair<const Key, Value>`


A relative question, but I want a solution without any run-time overhead. (So constructing a new pair or using std::variant are not the answers)


Due to the potential template specialization, reference has said pair<K, V> and pair<const K, V> are not similar, that means a simple reinterpret_cast would trigger undefined behaviour.

auto p1 = pair<int, double>{ 1, 2.3 };
auto& p2 = reinterpret_cast<pair<const int, double>&>(p1); // UB!

Type-punning through union works fine in C, but not always legal in C++:

But there's an exception (to be consistent with behaviours in C?):

If two union members are standard-layout types, it's well-defined to examine their common subsequence on any compiler.

Since Key and Value may not be standard-layout and may have non-trivial destructor, it seems type-punning here is impossible, though members of pair<Key, Value> and pair<const Key, Value> could share the same lifetime (of course with alignment assertion).

template <typename Key, typename Value>
union MapPair {
    using TrueType = pair<Key, Value>;
    using AccessType = pair<const Key, Value>;

    static_assert(
        offsetof(TrueType, first) == offsetof(AccessType, first)
     && offsetof(TrueType, second) == offsetof(AccessType, second)
     && sizeof(TrueType) == sizeof(AccessType)
    );

    TrueType truePair;
    AccessType accessPair;

    ~MapPair() {
        truePair.~pair();
    }

    // constructors for `truePair`
};

//...

auto mapPair = MapPair<NonTrivialKey, NonTrivialValue>{/*...*/};

// UB? Due to the lifetime of `truepair` is not terminated?
auto& accessPair = reinterpret_cast<pair<const NonTrivialKey, NonTrivialValue>&>(mapPair);

// still UB? Although objects on the buffer share the same constructor/destructor and lifetime
auto* accessPairPtr = std::launder(reinterpret_cast<pair<const NonTrivialKey, NonTrivialValue>*>(&mapPair));


I've noticed the guarantee that no elements are copied or moved when calling std::map::extract, and user-defined specilization of std::pair would cause UB when operating Node handle. So I trust some similar behaviours (type-punning or const_cast) really exist in the STL implementations relating to Node handle.

In libc++, it seems to depend on the characteristic of clang (doesn't optimize for data members), not the standard.

libstdc++ did the similar work as libc++, but no std::launder to refresh the type state.

MSVC is ... very surprising... and the commit history is too short that I can't find any reasons to support such a simple aliasing...


Is there a standard way here?


Solution

  • This is impossible. The node_handle proposal mentioned this as a motivation for standardizing it:

    One of the reasons the Standard Library exists is to write non-portable and magical code that the client can’t write in portable C++ (e.g. , , <type_traits>, etc.). This is just another such example.

    Note that the key member function is the only place where such tricks are necessary, and that no changes to the containers or pair are required.

    ref