23

I'm curious about conventions for type-punning pointers/arrays in C++. Here's the use case I have at the moment:

Compute a simple 32-bit checksum over a binary blob of data by treating it as an array of 32-bit integers (we know its total length is a multiple of 4), and then summing up all values and ignoring overflow.

I would expect such an function to look like this:

uint32_t compute_checksum(const char *data, size_t size)
{
    const uint32_t *udata = /* ??? */;
    uint32_t checksum = 0;
    for (size_t i = 0; i != size / 4; ++i)
        checksum += udata[i];
    return udata;
 }

Now the question I have is, what do you consider the "best" way to convert data to udata?

C-style cast?

udata = (const uint32_t *)data

C++ cast that assumes all pointers are convertible?

udata = reinterpret_cast<const uint32_t *>(data)

C++ cast that between arbitrary pointer types using intermediate void*?

udata = static_cast<const uint32_t *>(static_cast<const void *>(data))

Cast through a union?

union {
    const uint32_t *udata;
    const char *cdata;
};
cdata = data;
// now use udata

I fully realize that this will not be a 100% portable solution, but I am only expecting to use it on a small set of platforms where I know it works (namely unaligned memory accesses and compiler assumptions on pointer aliasing). What would you recommend?

Tom
  • 10,689
  • 4
  • 41
  • 50

4 Answers4

12

As far as the C++ standard is concerned, litb's answer is completely correct and the most portable. Casting const char *data to a const uint3_t *, whether it be via a C-style cast, static_cast, or reinterpret_cast, breaks the strict aliasing rules (see Understanding Strict Aliasing). If you compile with full optimization, there's a good chance that the code will not do the right thing.

Casting through a union (such as litb's my_reint) is probably the best solution, although it does technically violate the rule that if you write to a union through one member and read it through another, it results in undefined behavior. However, practically all compilers support this, and it results in the the expected result. If you absolutely desire to conform to the standard 100%, go with the bit-shifting method. Otherwise, I'd recommend going with casting through a union, which is likely to give you better performance.

Cheshar
  • 571
  • 1
  • 7
  • 21
Adam Rosenfield
  • 390,455
  • 97
  • 512
  • 589
  • litb's solutions is correct by the standard - but as I said, I'm looking at specific platforms already. – Tom Dec 07 '08 at 07:01
  • i'm not sure why they downvote this:) but i'm also not sure that my use of the union is undefined behavior.i'm aware that writing to a member and reading from another member is undefined behavior.but in my case, i'm pointing to a member of it,which is assumed to have a valid value,and read it then. – Johannes Schaub - litb Dec 07 '08 at 12:44
  • 2
    I don't think this particular example does break strict aliasing. char* is a special case under strict aliasing rules - a char* may never be assumed not an alias of a pointer to some other type. But in my answer I still play safe: it's just not worth doing char* differently from other similar cases. – Steve Jessop Dec 07 '08 at 14:07
  • 2
    onebyone, type punning is not the issue, it's already solved by the union. but the issue is the reading from the union member even though we didnt write to it before. the standard does not seem to forbid it. but that's the question we are unsure about :/ – Johannes Schaub - litb Dec 07 '08 at 14:20
  • 1
    @Adam, i have decided to delete my answer, because i found i don't agree with most of its interpretations anymore. But i still agree with your stuff in this answer. – Johannes Schaub - litb May 22 '09 at 22:35
  • 1
    @litb: What I meant is (if I can remember back that far): contrary to what Adam says, casting `const char*` to `const uint32_t*` *does not* break strict aliasing rules. Aliasing a `uint32_t*` with a `char*` is allowed, and safe, and optimisation doesn't change that. – Steve Jessop Nov 20 '09 at 16:28
  • 3
    @SteveJessop, casting to const char * is allowed by strict aliasing, but casting to uint32_t* is not allowed by strict aliasing. Explanation point 5 in http://en.cppreference.com/w/cpp/language/reinterpret_cast shows this with T2 as the target. The Type Aliasing section below says T2 must be char or unsigned char. So, converting to unsigned int * is indeed forbidden by strict aliasing. – D. A. Oct 06 '14 at 19:42
  • `memcpy` optimizes away with normal compilers (at least when the target ISA supports unaligned loads/stores, which most important ones do these days), and is the safe way to do an unaligned may-alias load. (or store). **Unions are not safe in C++**; a few real-world compilers like Sun's do in practice break the union idiom. https://blog.regehr.org/archives/959 In C++20, you might be able to use `std::bit_cast` between an array of `char[4]`, but memcpy is probably actually easier when you want to read multiple chars instead of pun between equal-sized things. – Peter Cordes Mar 22 '21 at 20:14
6

Ignoring efficiency, for simplicity of code I'd do:

#include <numeric>
#include <vector>
#include <cstring>

uint32_t compute_checksum(const char *data, size_t size) {
    std::vector<uint32_t> intdata(size/sizeof(uint32_t));
    std::memcpy(&intdata[0], data, size);
    return std::accumulate(intdata.begin(), intdata.end(), 0);
}

I also like litb's last answer, the one that shifts each char in turn, except that since char might be signed, I think it needs an extra mask:

checksum += ((data[i] && 0xFF) << shift[i % 4]);

When type punning is a potential issue, I prefer not to type pun rather than to try to do so safely. If you don't create any aliased pointers of distinct types in the first place, then you don't have to worry what the compiler might do with aliases, and neither does the maintenance programmer who sees your multiple static_casts through a union.

If you don't want to allocate so much extra memory, then:

uint32_t compute_checksum(const char *data, size_t size) {
    uint32_t total = 0;
    for (size_t i = 0; i < size; i += sizeof(uint32_t)) {
        uint32_t thisone;
        std::memcpy(&thisone, &data[i], sizeof(uint32_t));
        total += thisone;
    }
    return total;
}

Enough optimisation will get rid of the memcpy and the extra uint32_t variable entirely on gcc, and just read an integer value unaligned, in whatever the most efficient way to do that is on your platform, straight out of the source array. I'd hope the same is true of other "serious" compilers. But this code is now bigger than litb's, so there's not much to be said for it other than mine is easier to turn into a function template that will work just as well with uint64_t, and mine works as native endian-ness rather than picking little-endian.

This is of course not completely portable. It assumes that the storage representation of sizeof(uint32_t) chars corresponds to the storage representation of a uin32_t in the way we want. This is implied by the question, since it states that one can be "treated as" the other. Endian-ness, whether a char is 8 bits, and whether uint32_t uses all bits in its storage representation can obviously intrude, but the question implies that they won't.

Steve Jessop
  • 273,490
  • 39
  • 460
  • 699
  • 1
    I just tried your last example. GCC refuses to vectorize it, complaining about "unhandled data-ref". – Tom Dec 07 '08 at 16:09
  • Fair enough, it's not the fastest possible on hardware supporting vector ops. Maybe GCC will get better in future. My extreme programming guru says I don't need to lose sleep over that. My first suggestion isn't as fast as possible on *any* hardware :-) – Steve Jessop Dec 07 '08 at 19:59
  • 2
    But I agree that since I mentioned the memcpy being low-cost, the fact that it prevents vectorization could be a show-stopper for some applications. – Steve Jessop Dec 07 '08 at 20:04
  • Sadly, 8 years later GCC still [doesn't vectorize it](https://godbolt.org/g/Xg9g7G), unlike type-punned version... – Ruslan Aug 26 '16 at 17:07
  • GCC now also [vectorises](https://gcc.godbolt.org/z/gB-4Mz) the memcpy version. – Dino Nov 15 '19 at 08:31
  • `memcpy` on one uint32 at a time into a local will optimize away. Copying *all* the bytes with one large memcpy, especially into dynamically-allocated storage (a std::vector) likely won't. Or in GNU C, use `typedef uint32_t aliasing_unaligned_u32 __attribute__((aligned(1),may_alias))` like in [Why does glibc's strlen need to be so complicated to run quickly?](https://stackoverflow.com/a/57676035). Anyway, the first recommendation in your answer, the big memcpy, is clearly very bad and should be removed. – Peter Cordes Mar 22 '21 at 19:58
  • (To be fair, if compiling for a platform without single-instruction unaligned loads in asm (e.g. classic MIPS), even a 4-byte memcpy might not inline with GCC, in which case one big copy to a std::vector might be the lesser of two evils if you had to choose between these two, although certainly not the optimal asm solution.) – Peter Cordes Mar 22 '21 at 20:01
0

There are my fifty cents - different ways to do it.

#include <iostream>
#include <string>
#include <cstring>

    uint32_t compute_checksum_memcpy(const char *data, size_t size)
    {
        uint32_t checksum = 0;
        for (size_t i = 0; i != size / 4; ++i)
        {
            // memcpy may be slow, unneeded allocation
            uint32_t dest; 
            memcpy(&dest,data+i,4);
            checksum += dest;
        }
        return checksum;
    }

    uint32_t compute_checksum_address_recast(const char *data, size_t size)
    {
        uint32_t checksum = 0;
        for (size_t i = 0; i != size / 4; ++i)
        {
            //classic old type punning
            checksum +=  *(uint32_t*)(data+i);
        }
        return checksum;
    }

    uint32_t compute_checksum_union(const char *data, size_t size)
    {
        uint32_t checksum = 0;
        for (size_t i = 0; i != size / 4; ++i)
        {
            //Syntax hell
            checksum +=  *((union{const char* c;uint32_t* i;}){.c=data+i}).i;
        }
        return checksum;
    }

    // Wrong!
    uint32_t compute_checksum_deref(const char *data, size_t size)
    {
        uint32_t checksum = 0;
        for (size_t i = 0; i != size / 4; ++i)
        {
            checksum +=  *&data[i];
        }
        return checksum;
    }

    // Wrong!
    uint32_t compute_checksum_cast(const char *data, size_t size)
    {
        uint32_t checksum = 0;
        for (size_t i = 0; i != size / 4; ++i)
        {
            checksum +=  *(data+i);
        }
        return checksum;
    }


int main()
{
    const char* data = "ABCDEFGH";
    std::cout << compute_checksum_memcpy(data, 8) << " OK\n";
    std::cout << compute_checksum_address_recast(data, 8) << " OK\n";
    std::cout << compute_checksum_union(data, 8) << " OK\n";
    std::cout << compute_checksum_deref(data, 8) << " Fail\n";
    std::cout << compute_checksum_cast(data, 8) << " Fail\n";
}
Петър Петров
  • 1,966
  • 1
  • 17
  • 14
  • 2
    -1 for ye olde C casts and not pointing out that type-punning via a `union` is undefined behaviour as only the last-accessed member has a Standard-determined value – underscore_d Dec 31 '15 at 12:56
  • `*(uint32_t*)(data+i);` is unsafe even in C (except for MSVC, or `gcc -fno-strict-aliasing`). Also, the union type-punning is putting the *pointer* in the union, not the data! You want a union of `{char c[4]; uint32_t u;}` if you want C99 union type punning. (Which is supported in C++ by many but not all real-world implementation. Including G++ / clang++, but I think I've heard not Sun's compiler.) – Peter Cordes Mar 22 '21 at 20:05
-3

I know this thread has been inactive for a while, but thought I'd post a simple generic casting routine for this kind of thing:

// safely cast between types without breaking strict aliasing rules
template<typename ReturnType, typename OriginalType>
ReturnType Cast( OriginalType Variable )
{
    union
    {
        OriginalType    In;
        ReturnType      Out;
    };

    In = Variable;
    return Out;
}

// example usage
int i = 0x3f800000;
float f = Cast<float>( i );

Hope it helps someone!

Hybrid
  • 396
  • 4
  • 20
  • 4
    Type punning is undefined in the standard. However, type punning using unions are supported by at least GCC (with strict-aliasing enabled iirc). – Jens Åkerblom May 18 '13 at 10:53
  • 2
    Is it undefined? My understanding is that it is unspecified behaviour, only if the sizes of the types differ, not undefined. Assert that the sizeof's match, for safety. (http://stackoverflow.com/questions/11639947/is-type-punning-through-a-union-unspecified-in-c99-and-has-it-become-specified) – Hybrid May 23 '13 at 17:25
  • 4
    @Hybrid that answer is about C, not C++. The behavior is radically different between C and C++. – The Paramagnetic Croissant May 01 '14 at 21:10
  • 1
    Type punning used in unions is perfectly valid and working case in all compilers: GCC, MSVC, LLVM. However, you will have to handle the only one issue: Endianess. "Byte Order Masks" will help you for this. – Петър Петров Nov 29 '15 at 00:23
  • 1
    @ПетърПетров, no, stop. "perfectly valid and working case in **the implementation-defined case of these 3 compilers I have tested**" does **not** equate to defined, guaranteed, portable, Standard-compliant behaviour. Relying on that implementation-defined behaviour then becomes a massive risk to portability that in most cases is really not worth taking. Certainly don't indicate to readers that it is safe in this way. – underscore_d Dec 31 '15 at 12:53
  • @JensÅkerblom More than IYRC, do you have a link to where `g++` makes a provision for this? I'll rely on implementation-defined behaviour if I _must_, but I need to know it is defined somewhere. – underscore_d Jan 05 '16 at 10:47
  • My bad, the proper term here is definitely not "perfectly valid and working case", it's only "Perfectly working case in the implementations of mentioned compilers", AND You have to deal with endianess yourself! About g++, I don't think it's defined, but this compiler already compiles a lot of games that use union-type-punning, so does LLVM – Петър Петров Jan 07 '16 at 14:15
  • 1
    @underscore_d: Type-punning through a union is so important an idiom in both C and C++ that it is not a big risk for portability, regardless of what the standards say. – Erik Alapää Jan 30 '17 at 14:22
  • 1
    @underscore_d: https://gcc.gnu.org/onlinedocs/gcc/Structures-unions-enumerations-and-bit-fields-implementation.html confirms that GNU C90 defines the union type-pun behaviour, not just C99 and later. (Also applies to the GNU dialect of C++, as mentioned in https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html#Type-punning). https://blog.regehr.org/archives/959 confirms that GNU C++ (including clang) makes union type-punning safe. But also points out that some real-world C++ implementations do *not* make it safe, notably Sun's compiler. Fortunately C++20 finally has `std::bit_cast<>` – Peter Cordes Mar 22 '21 at 20:10