11

I'm trying to build a class template that packs a bunch of types in a suitably large char array, and allows access to the data as individual correctly typed references. Now, according to the standard this can lead to strict-aliasing violation, and hence undefined behavior, as we're accessing the char[] data via an object that is not compatible with it. Specifically, the standard states:

If a program attempts to access the stored value of an object through a glvalue of other than one of the following types the behavior is undefined:

  • the dynamic type of the object,
  • a cv-qualified version of the dynamic type of the object,
  • a type similar (as defined in 4.4) to the dynamic type of the object,
  • a type that is the signed or unsigned type corresponding to the dynamic type of the object,
  • a type that is the signed or unsigned type corresponding to a cv-qualified version of the dynamic type of the object,
  • an aggregate or union type that includes one of the aforementioned types among its elements or non-static data members (including, recursively, an element or non-static data member of a subaggregate or contained union),
  • a type that is a (possibly cv-qualified) base class type of the dynamic type of the object,
  • a char or unsigned char type.

Given the wording of the highlighted bullet point, I came up with the following alias_cast idea:

#include <iostream>
#include <type_traits>

template <typename T>
T alias_cast(void *p) {
    typedef typename std::remove_reference<T>::type BaseType;
    union UT {
        BaseType t;
    };
    return reinterpret_cast<UT*>(p)->t;
}

template <typename T, typename U>
class Data {
    union {
        long align_;
        char data_[sizeof(T) + sizeof(U)];
    };
public:
    Data(T t = T(), U u = U()) { first() = t; second() = u; }
    T& first() { return alias_cast<T&>(data_); }
    U& second() { return alias_cast<U&>(data_ + sizeof(T)); }
};


int main() {
    Data<int, unsigned short> test;
    test.first() = 0xdead;
    test.second() = 0xbeef;
    std::cout << test.first() << ", " << test.second() << "\n";
    return 0;
}

(The above test code, especially the Data class is just a dumbed-down demonstration of the idea, so please don't point out how I should use std::pair or std::tuple. The alias_cast template should also be extended to handle cv qualified types and it can only be safely used if the alignment requirements are met, but I hope this snippet is enough to demonstrate the idea.)

This trick silences the warnings by g++ (when compiled with g++ -std=c++11 -Wall -Wextra -O2 -fstrict-aliasing -Wstrict-aliasing), and the code works, but is this really a valid way of telling the compiler to skip strict-aliasing based optimizations?

If it's not valid, then how would one go about implementing a char array based generic storage class like this without violating the aliasing rules?

Edit: replacing the alias_cast with a simple reinterpret_cast like this:

T& first() { return reinterpret_cast<T&>(*(data_ + 0)); }
U& second() { return reinterpret_cast<U&>(*(data_ + sizeof(T))); }

produces the following warning when compiled with g++:

aliastest-so-1.cpp: In instantiation of ‘T& Data::first() [with T = int; U = short unsigned int]’: aliastest-so-1.cpp:28:16:
required from here aliastest-so-1.cpp:21:58: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing]

dirkgently
  • 108,024
  • 16
  • 131
  • 187
mitchnull
  • 6,161
  • 2
  • 31
  • 23
  • Two notes: 1) Shouldn't `alias_cast` handle the pointer arithmetic by itself? 2) You can access individual bytes and then perhaps build up the final object through something like `memcpy`. Of course, this will not work for anything with a non-trivial ctor. Are you looking to support generic types or just fundamental type? This is for some packed tuple class, is it? – dirkgently Jun 13 '12 at 08:42
  • @dirkgently yes, it's something like a packed tuple, and `memcopy` would be the "safe" bet indeed, I just try to avoid it due to performance reasons if I can get away with it :) – mitchnull Jun 13 '12 at 08:46
  • Correct me if I'm wrong, but any packed representation will also require a bit of `placement-new`-ing around. This is what you should be worried the most. You cannot possibly have pointers that are mis-aligned. *Finally*, I believe that you are looking for dynamic layouting of types to minimize space? If you're ready to allow for platform specific `pragma`-tic coding, you can probably get away with an optimally packed representation. OTOH, if you're looking for *the most generic implementation possible* you'll probably have to account for some padding. – dirkgently Jun 13 '12 at 08:52
  • I understand my comments are useless for you but I'm hoping they may help others help you! Cheers :) – dirkgently Jun 13 '12 at 08:52
  • 1
    @mitchnull: the real question is, if you respect alignment and sizes, then you are reimplementing the logic that the compiler already provides for a regular structure. What do you gain ? – Matthieu M. Jun 13 '12 at 09:08
  • @MatthieuM. I'm working on a generic VertexData structure for OpenGL, where I can assemble the VertexData by gluing together its components, like `VertexData`. The above implementation is one approach (it has the aliasing issue). The other approach is using a `std::tuple`, but then getting the offset of each element is problematic, as `std::tuple` is not standard-layout. – mitchnull Jun 13 '12 at 09:19
  • @dirkgently I'm dealing with simple types, so no real need for `placement-new`. I'll of course check pointer alignment, I'll revert to `memcpy` in case the pointer would not be suitable via some SFINAE magic. And yes, I'm aiming for a generic standard conforming implementation, not some compiler specific hack. – mitchnull Jun 13 '12 at 09:22
  • 2
    @mitchnull: interesting, maybe developping a lightweight tuple would be easier then. I wonder if is possible though. Otherwise, do you know about `std::aligned_storage` ? It was introduced in C++11 for manipulating raw memory and still get size and alignment right :) – Matthieu M. Jun 13 '12 at 12:12
  • @MatthieuM. Yes, I know about `std::aligned_storage`. incidently, I don't see how it gets around the same aliasing issue I have :) Regarding the lightweight tuple implementation: I don't think a standard-layout tuple implementation is possible... – mitchnull Jun 13 '12 at 12:39
  • 4
    @mitchnull: I don't understand why there should be aliasing issue since `char*` and `void*` are special-cased in this regard. – Matthieu M. Jun 13 '12 at 15:09
  • Have you considered `volatile`? – curiousguy Jul 15 '12 at 00:22
  • @curiousguy How could `volatile` possible be of any help in this situation? – underscore_d Aug 27 '16 at 19:08
  • @underscore_d By killing all optimisations, obviously. – curiousguy Aug 31 '16 at 21:33
  • That warning just completely gone since (at least) GCC 4.5.3, even with -Wstrict-aliasing=1. Is this warning just an old bug in GCC? [LIVE](https://godbolt.org/g/z9NWPD) – sandthorn Feb 03 '18 at 09:25

1 Answers1

3

Using a union is almost never a good idea if you want to stick with strict conformance, they have stringent rules when it comes to reading the active member (and this one only). Although it has to be said that implementations like to use unions as hooks for reliable behaviour, and perhaps that is what you are after. If that is the case I defer to Mike Acton who has written a nice (and long) article on aliasing rules, where he does comment on casting through a union.

To the best of my knowledge this is how you should deal with arrays of char types as storage:

// char or unsigned char are both acceptable
alignas(alignof(T)) unsigned char storage[sizeof(T)];
::new (&storage) T;
T* p = static_cast<T*>(static_cast<void*>(&storage));

The reason this is defined to work is that T is the dynamic type of the object here. The storage was reused when the new expression created the T object, which operation implicitly ended the lifetime of storage (which happens trivially as unsigned char is a, well, trivial type).

You can still use e.g. storage[0] to read the bytes of the object as this is reading the object value through a glvalue of unsigned char type, one of the listed explicit exceptions. If on the other hand storage were of a different yet still trivial element type, you could still make the above snippet work but would not be able to do storage[0].

The final piece to make the snippet sensible is the pointer conversion. Note that reinterpret_cast is not suitable in the general case. It can be valid given that T is standard-layout (there are additional restrictions on alignment, too), but if that is the case then using reinterpret_cast would be equivalent to static_casting via void like I did. It makes more sense to use that form directly in the first place, especially considering the use of storage happens a lot in generic contexts. In any case converting to and from void is one of the standard conversions (with a well-defined meaning), and you want static_cast for those.

If you are worried at all about the pointer conversions (which is the weakest link in my opinion, and not the argument about storage reuse), then an alternative is to do

T* p = ::new (&storage) T;

which costs an additional pointer in storage if you want to keep track of it.

I heartily recommend the use of std::aligned_storage.

Luc Danton
  • 34,649
  • 6
  • 70
  • 114
  • Ah, so I can change the dynamic type of the storage with placement new. Can I do that "in the middle" of the array, too? From the example, can I change the first half of my `data_` array to type `T` and the second half to type `U`? – mitchnull Jun 14 '12 at 06:54
  • I changed the above code to use placement new in the constructor, and replaced the `alias_cast` with the cast-thru `void*`, but that cast still triggers the strict-aliasing warning in g++... – mitchnull Jun 14 '12 at 07:07
  • @mitchnull You're setting up yourself for some difficulties if you want to handle alignment by yourself when the compiler would do it for you if you were to use a standard-layout class combined with `offsetof`. Can you do that instead? – Luc Danton Jun 14 '12 at 10:32
  • "_ended the lifetime of storage (which happens trivially as unsigned char is a, well, trivial type)._" I don't think you can "end the lifetime" of an object of a builtin type. – curiousguy Jul 14 '12 at 18:15
  • 1
    @curiousguy C++ doesn't have a notion of a 'builtin type'. 3.8 paragraph 1 covers end of lifetime very explicitly: "The lifetime of an object of type T ends when: — if T is a class type with a non-trivial destructor (12.4), the destructor call starts, or — the storage which the object occupies is reused or released." – Luc Danton Jul 14 '12 at 20:51
  • @LucDanton "_C++ doesn't have a notion of a 'builtin type'._" `char` `short` `float` `T*` ... how do you call that? "_3.8 paragraph 1_" I disagree. What is the lifetime of an `int` object anyway? – curiousguy Jul 14 '12 at 23:03
  • I think that "3.8 paragraph 1" is just junk. In particular, the "the destructor call **starts**" part is utter non sense (should be "the destructor call **ends**". This is enough to cast serious doubt on the whole list. – curiousguy Jul 14 '12 at 23:14
  • 1
    "_Note that reinterpret_cast is not suitable in the general case. _" I am absolutely sure it is appropriate in very case. If the standard does not say so it is clearly broken. – curiousguy Jul 14 '12 at 23:16
  • "_char short float T* ... how do you call that?_" I think it is "scalar". – curiousguy Jul 15 '12 at 00:25
  • @LucDanton Very good summary. However, I think your concerns about `reinterpret_cast` are unfounded, at least retroactively. :-) Recent revisions of the Standard make clear that in appropriate situations, `reinterpret_cast(&u)` is exactly equivalent to `static_cast( static_cast(&u) )`... and mercifully half the typing and brackets. – underscore_d Aug 27 '16 at 19:05
  • 1
    @underscore_d even better, it's slated to mean exactly that in all circumstances in the upcoming C++1z ([perishable link to draft](http://eel.is/c++draft/expr.reinterpret.cast#7)) – Luc Danton Aug 27 '16 at 21:51
  • @LucDanton Yeah, that's the wording I know, from whenever it arose. I guess the vague concept I was thinking about in my previous comment, about there being exclusions, was misremembered and based on the other, non-pointer (mis?)uses of `reinterpret_cast`... which wouldn't have mattered in this example. Anyway, yeah, for casting between object pointer types, they're now equivalent, which is very handy. – underscore_d Aug 28 '16 at 15:27