Packing and unpacking two 32-bit integers into an unsigned 64-bit integer Does this look good for combining/retrieving two 32 bit integers (a type and an index) into/from an unsigned 64 bit integer (a unique ID)?

The point is to hide in the API the composition of the unique ID and allow its calculation to change later without changing the API (e.g. possibility to change later to consecutive 64 bit ids without changing API).

#include &lt;limits&gt;

uint64_t combine(uint32_t low, uint32_t high)
{
     return (((uint64_t) high) &lt;&lt; 32) | ((uint64_t) low);
}

uint32_t high(uint64_t combined)
{
    return combined &gt;&gt; 32;
}

uint32_t low(uint64_t combined)
{
    uint64_t mask = std::numeric_limits&lt;uint32_t&gt;::max();
    return mask &amp; combined; // should I just do "return combined;" which gives same result?
}


Or would a union approach like below be better? Is this union guaranteed to fit into 64 bits (e.g. guarantee no padding in the struct)?

union Id
{
    struct
    {
       uint32_t index; // lower 32 bits
       uint32_t type;  // upper 32 bits
    } split;

    uint64_t unique_id;
};


Here is some code I used to test:

#include &lt;iostream&gt;
#include &lt;assert.h&gt; 
#include &lt;sstream&gt;

template &lt;typename T&gt;
std::string bits(T num)
{
    const int num_bits = sizeof(num) * 8;

    T maxPow = T(1) &lt;&lt; (num_bits - 1);

    std::stringstream ss;

    for(int i=0; i &lt; num_bits; ++i)
    {
        // print last bit and shift left.
        ss &lt;&lt; (num &amp; maxPow ? 1 : 0);
        if ((i+1) % 8 == 0) ss &lt;&lt; " ";
        num = num &lt;&lt; 1;
    }

    return ss.str();
}

void test1()
{
    {
        int in_low = -3;
        int in_high = 99;
        uint64_t combined = combine(in_low, in_high);
        assert( bits(in_high) + bits(in_low) == bits(combined) );
        assert( in_low == low(combined) );
        assert( in_high == high(combined) );
    }

    {
        uint32_t in_low = 3;
        int in_high = -99;
        uint64_t combined = combine(in_low, in_high);
        assert( bits(in_high) + bits(in_low) == bits(combined) );
        assert( in_low == low(combined) );
        assert( in_high == high(combined) );
    }

    {
        uint32_t in_low = std::numeric_limits&lt;uint32_t&gt;::max();
        int in_high = std::numeric_limits&lt;int32_t&gt;::min();
        uint64_t combined = combine(in_low, in_high);
        assert( bits(in_high) + bits(in_low) == bits(combined) );
        assert( in_low == low(combined) );
        assert( in_high == high(combined) );
    }    
}

void test2() {
    Id in; // would "Id in = {-3, 99};" be better? Is this legal C++03? Would a constructor be better?
    in.split.type = -3; 
    in.split.index = 99;

    std::cout &lt;&lt; in.unique_id &lt;&lt; std::endl; // prints 18446744060824649827
    std::cout &lt;&lt; bits(in.split.type) &lt;&lt; bits(in.split.index) &lt;&lt; std::endl;
    std::cout &lt;&lt; bits(in.unique_id) &lt;&lt; std::endl; // prints 11111111 11111111 11111111 11111101 00000000 00000000 00000000 01100011
    assert(bits(in.split.type) + bits(in.split.index) == bits(in.unique_id));

    Id out;
    out.unique_id = in.unique_id;
    std::cout &lt;&lt; int(out.split.type) &lt;&lt; ", " &lt;&lt; out.split.index &lt;&lt; std::endl; // prints -3, 99

    assert(in.split.type == out.split.type);
    assert(in.split.index == out.split.index);
}
Union provides no guarantee of memory arrangement for this.  The C++ standard merely says that it's no bigger than the storage required for the largest element, and that it's designed to only have one active data member.

If you are only targeting one compiler and one architecture it may be okay to use it to translate between types, but it is definitely skiing off-piste.

I'd be tempted to wrap the whole thing up in a class and provide getter methods.  This insulates the user from changes in the implementation of the API in the most flexible way. And, use the bitwise operations (>>, &lt;&lt;, | and &amp;) inside the class.  These operations will happen on a register, rather than in memory, and therefore do not suffer for endian issues.