Generic char[] based storage and avoiding strict-a

2019-03-25 05:47发布

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]

1条回答
看我几分像从前
2楼-- · 2019-03-25 06:25

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.

查看更多
登录 后发表回答