2
\$\begingroup\$

This question is about swapping bytes of an uint64_t value, or in other words, about converting between endianness of an integer value:

#include <algorithm>
#include <cstdint>
#include <iomanip>
#include <iostream>

uint64_t& swapBytes1(uint64_t& num) {
    using std::swap;

    union {
        uint8_t bytes[8];
        uint64_t num;
    } transformer;

    transformer.num = num;

    swap(transformer.bytes[0], transformer.bytes[7]);
    swap(transformer.bytes[1], transformer.bytes[6]);
    swap(transformer.bytes[2], transformer.bytes[5]);
    swap(transformer.bytes[3], transformer.bytes[4]);

    return num = transformer.num;
}

uint64_t& swapBytes2(uint64_t& num) {
    using std::swap;

    union {
        struct {
            uint8_t b0;
            uint8_t b1;
            uint8_t b2;
            uint8_t b3;
            uint8_t b4;
            uint8_t b5;
            uint8_t b6;
            uint8_t b7;
        } bytes;

        uint64_t num;
    } transformer;

    transformer.num = num;

    swap(transformer.bytes.b0, transformer.bytes.b7);
    swap(transformer.bytes.b1, transformer.bytes.b6);
    swap(transformer.bytes.b2, transformer.bytes.b5);
    swap(transformer.bytes.b3, transformer.bytes.b4);

    return num = transformer.num;
}

void main() {
    uint64_t num = 0x0123456789abcdef;
    std::cout << std::hex << num << "\n";
    std::cout << swapBytes1(num) << "\n";
    swapBytes2(num);
    std::cout << num << "\n";
}

Critique request

As I am not a professional C++ developer, I would like to receive any comment/answer that could improve my C++ coding habits.

\$\endgroup\$
5
  • 1
    \$\begingroup\$ See this SO answer. \$\endgroup\$
    – pacmaninbw
    Commented Dec 11, 2020 at 13:56
  • \$\begingroup\$ What are you doing with the answer? Obviously this has huge potential for being non-portable. Did you consider using std::reverse for the 1st version? \$\endgroup\$ Commented Dec 11, 2020 at 17:07
  • \$\begingroup\$ Though according to the standard this is technically UB (putting data in one side of a union and pulling it from another). If this did not work so much code that is live would break. \$\endgroup\$ Commented Dec 11, 2020 at 17:52
  • 1
    \$\begingroup\$ There are already standard functions for doing this: htonl() and family. linux.die.net/man/3/htonl \$\endgroup\$ Commented Dec 11, 2020 at 17:53
  • \$\begingroup\$ Added reinventing-the-wheel tag. \$\endgroup\$
    – coderodde
    Commented Dec 11, 2020 at 17:55

1 Answer 1

4
\$\begingroup\$

Technically this is UB.

Writing a value to one member of a union, then reading from another member (that was not written to) of the union is undefined behavior.

You would be better off just casting to a char* and just reversing it.

swap(std::uint64_t& num)
{
    // I like the use of reinterpret_cast here
    // it makes sure that somebody reading the code notices
    // and takes special care to review what is happening.
    //
    char* first = reinterpret_cast<char*>(&num);
    char* last  = first + sizeof(num);

    // The advantage of reverse here:
    //
    // The standard library can have pre built optimizations
    // that will use hardware specific optimizations of this that
    // can convert it to a single instruction on some hardware.
    //
    std::reverse(first, last);
}

The question becomes are you doing this to support endianness?

If so then your code is only going to work on machines that have the same endianness as the platform you are currently working on.

Normally people want to swap from platform byte order to network byte order (or the reverse). Depending on architecture this is either a swap (as you have above) or a NO-OP or for some weird hardware something completely different.

To support this we have specific family of functions:

htonl()   // host to network long  (32 byte)
htons()   // host to network short (16 byte)

ntohl()   // network to host long  (32 byte)
ntohs()   // network to host short (16 byte)

These functions will do the appropriate conversion for your hardware.

\$\endgroup\$
2
  • \$\begingroup\$ htonl() only does 32 bits, htons() 16 bits. There might be non-standard functions to handle 64 bits at a time, like bswap_64() in glibc, or builtins like __builtin_bswap64() in GCC. \$\endgroup\$
    – G. Sliepen
    Commented Dec 11, 2020 at 19:59
  • \$\begingroup\$ @G.Sliepen Anyway, those functions solve a different problem, that on little-endian platforms reduces to this. On big-endian platforms, they are identity-functions. \$\endgroup\$ Commented Dec 12, 2020 at 1:23

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.