1

I have a template class with a bool as its template parameter Dynamic<bool>. Whether the parameter is true or false it has the exact same data members. they just differ in their member functions.

There is one situation that I need to convert one to another temporarily, instead of using a copy/move constructor. So I resorted to type-punning. To make sure that it cause an issue I used two static_asserts:

d_true=Dynamic<true>(...);
...
static_assert(sizeof(Dynamic<true>)==sizeof(Dynamic<false>),"Dynamic size mismatch");
static_assert(alignof(Dynamic<true>)==alignof(Dynamic<false>),"Dynamic align mismatch");
Dynamic<false>& d_false=*reinterpret_cast<Dynamic<false>*>(&d_true);
...

So I think what I am doing is safe, and if anything is about to go wrong the compiler will give me a static_assert error. However, gcc gives a warning:

warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing]

My question is twofold: is what I am doing the best way to achieve this? If it is, how can convince gcc it is safe, and get rid of the warning?

phuclv
  • 37,963
  • 15
  • 156
  • 475
Lawless
  • 75
  • 1
  • 5
  • 4
    You're not allowed to do type punning like that in C++. – druckermanly Aug 02 '19 at 00:22
  • No, what you're doing is not the best way to achieve this. The best way to achieve this does not involve type punning, but rather redesigning your templates and related classes so these kinds of gymnastics are not required. – Sam Varshavchik Aug 02 '19 at 00:22

4 Answers4

3

One obvious possibility would be to separate the data that's common to both out into its own class (or struct), then grab that from the the object when you need it.

struct Common {
// ...
};

template <bool b>
class Dynamic { 
    Common c;
public:
    Common &get_data() { return c; }
    // ...
};

From there, the rest seems fairly obvious--when you need the data from the Dynamic<whatever>, you call get_data() and off you go.

Of course, there are variations on the general theme as well--for example, you could use inheritance instead:

struct Common { /* ... */ };

template <bool t>
class Dynamic : public Common {
    // ...
};

This eliminates the extra c. the previous version would need for every reference to the common data, but (at least in my opinion) inheritance is probably too high a price to pay for that.

Jerry Coffin
  • 476,176
  • 80
  • 629
  • 1,111
0

In the standard it's "forbidden" to reinterpret a region of memory from type A to type B. This is called aliasing. There are 3 exceptions to aliasing, same types with different CV qualification, base types, and regions of char[]. (and for char the derogation only works unidirectionaly in the direction to char)

If you use std::aligned_storage and placement new, you can reinterpret that region into anything you want without the compiler able to complain. That's how variant works.

EDIT: Ok the above is actually true (as long as you don't forget std::launder), but misleading because of "lifetime". Only one object may live on top of a storage space at once. So it's undefined behavior to interpret it through the view of another type, while it's alive. The key is the construction.

If I may suggest, go to cppreference, take their static_vector example, simplify it to the case of 1. Add a few getters, congratulations, you've reinvented bitcast :) (proposal http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0476r2.html).

Probably it'd look like this:

#include <type_traits>
#include <string>
#include <new>
#include <cstring>
#include <iostream>

using namespace std;

template< bool B >
struct Dynamic
{
    template <bool B2 = B>
    void ConditionalMethod(typename enable_if<B2>::type** = 0)
    {}

    string m_sharedObject = "stuff";
};

int main()
{
    using D0 = Dynamic<false>;
    using D1 = Dynamic<true>;
    aligned_storage<sizeof(D0), alignof(D0)>::type store[1];

    D0* inst0 = new (&store[0]) D0 ;

    // mutate
    inst0->m_sharedObject = "thing";

    // pune to D1
    D1* inst1 = std::launder(reinterpret_cast<D1*>(&store[0]));

    // observe aliasing
    cout << inst1->m_sharedObject;

    inst0->~D0();
}

see in effect in wandbox

EDIT: after a lengthy discussion there are other parts of the new standard than the section 'Types 8.2.1.11', that explains better why this isn't strictly valid. I recommend to refer to the "lifetime" chapter.
https://en.cppreference.com/w/cpp/language/lifetime
From Miles Budnek comment:

there is no Dynamic<true> object at that address, accessing it through a Dynamic<false> is undefined behavior.

v.oddou
  • 6,476
  • 3
  • 32
  • 63
  • 3
    `std::variant` doesn't do type punning. And afaik unions don't allow type punning in C++ (they do in C). – bolov Aug 02 '19 at 00:54
  • @bolov it does reinterpret a region of char into the type you're getting after having verified the current index corresponds to that type. that's not punning but if you want punning you'll have to go through a region of char. – v.oddou Aug 02 '19 at 00:58
  • 2
    @v.oddou No, it does not reinterpret an array of `char` as another type. It uses placement-new to construct a new object in uninitialized storage. Those are very different things. The former leads to undefined behavior, while the latter is well-defined. – Miles Budnek Aug 02 '19 at 01:04
  • @MilesBudnek no. https://www.boost.org/doc/libs/master/boost/type_traits/aligned_storage.hpp – v.oddou Aug 02 '19 at 01:19
  • @v.oddou what is that link suppose to prove? – M.M Aug 02 '19 at 01:42
  • @M.M that "unitialized storage" is not a very different thing from an array of char. array of char is the only way to create unitialized storage space (in auto duration storage). I don't understand the point of "very different"-iating them. `variant` uses `storage_t` which is essentially `std::aligned_storage` before it was standardized. And how that `storage_t` is implemented is `unsigned char[sizeof(T) + alignof(T)] space` and the `address()` function gets the first aligned pointer of that space. And this is not UB at all since the standard allows a derogation to aliasing for regions of char – v.oddou Aug 02 '19 at 01:46
  • 2
    The point is that the contents of the char array are never reinterpreted (which would be undefined behaviour). The purpose of the `aligned_storage` is so the caller can use placement-new to create new objects in the same region of storage (which ends the lifetime of the char array). Also the standard does NOT allow aliasing a char array as another type. It does allow aliasing other types as a char array. – M.M Aug 02 '19 at 01:51
  • @M.M ? what is it I'm under fire for here, the terminology "reinterpret" ? the source uses `static_cast` indeed, that makes it a different concept ? `aligned_storage` is not a primitive of the language, it's a library contraption, you will agree. replace it mentally with char array (+ tiny help on alignment for type T). What do you call "ending the lifetime of the char array" ? Do you mean something in the likes of the standard wording for unions: "at most one of the non-static data members can be active at any time" ? – v.oddou Aug 02 '19 at 01:59
  • @M.M "Also the standard does NOT allow aliasing a char array as another type" source ? – v.oddou Aug 02 '19 at 02:00
  • It's in the strict aliasing rule in the standard , it lists the allowed aliasing types and none of them is aliasing a char as a general type – M.M Aug 02 '19 at 02:08
  • Placement-new ends the lifetime of any object that previously existed at the location, that's what the standard says. – M.M Aug 02 '19 at 02:09
  • @M.M this list right https://stackoverflow.com/a/7005988/893406 ? "Placement-new ends the lifetime of the char array" > ok I'll go read that. – v.oddou Aug 02 '19 at 02:10
  • 2
    @v.oddou source: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4713.pdf §8.2.1 Value category [basic.lval] paragraph 11. You can access an object through a glvalue of type char, but not the other way around. – bolov Aug 02 '19 at 02:11
  • @bolov OOOOH that's it I see the issue here ! Ok ok ok ok, in the case of OP, his object already exists, and therefore was not reinterpreted out of a char array, therefore it can't be punned without being copied. That's why `bit_cast` does the memcopy. Ok and that explains why `variant` and `optional` work, because their storage is the char region, right from the start. Now the remaining question is if you already puned your char to T, do you have enough remaining mana to pune it to T2 while T is alive ? If the char array is dead by effect of `new T(p)`, that'd be compromised. – v.oddou Aug 02 '19 at 02:18
  • wait a minute. but that's what I recommended in my answer. so what's the problem ? – v.oddou Aug 02 '19 at 02:20
  • 3
    @v.oddou The point is that `variant` never reinterprets anything. If it changes the type it holds, it first destroys the old object and then constructs a new one. Both objects inhabit the same _storage_, but only one after the other. Nothing is punned or reinterpreted. OP wants to treat the _same object_ as a different type without copying it, and you simply can't do that (outside of very limited circumstances, none of which involve member functions). – Miles Budnek Aug 02 '19 at 03:00
  • @MilesBudnek ok, so you're saying that the idea presented in the code in my answer is illegal, right ? I'm opened to accept that, but it doesn't seem super obvious. On the contrary the language in §8.2.1 is the original impetus for my idea of using char to leverage legal aliasing. And in my example there is no retro-conversion from T to char (the illegal way that bolov pointed). If this method is really out of the question I'll have to delete my answer. But it's not clear that it is. – v.oddou Aug 02 '19 at 04:44
  • @v.oddou You've got it backwards. The byte-representation of any object may be accessed by casting a pointer to that object to a pointer to `char` (or `unsigned char` or `std::byte`). The opposite is not true. You _cannot_ cast a pointer to `char` to an unrelated type and access an object via the type-punned pointer. `char* cp = &someObject; char c = *cp;` is valid, `SomeType* someObjectPtr = someCharArray; SomeType someObject = *someObjectPtr;` is undefined behavior. See [\[basic.lval\]](https://timsong-cpp.github.io/cppwp/n4659/basic.lval#8). – Miles Budnek Aug 02 '19 at 05:01
  • @MilesBudnek come on, this is how `aligned_storage` works; it's doing just that. If that's UB, then the likes of `optional` are based on UB aren't they. Or are you telling me that the placement new in the middle makes it suddenly not aliasing ? – v.oddou Aug 02 '19 at 05:12
  • The standardese says «access the stored value of an object through a char is valid» I interpret your second example as falling into this case. It isn't clear that how you interpret the then-obtained char representation is constrained by that phrasing. – v.oddou Aug 02 '19 at 05:23
  • @v.oddou It's the difference between an _object_ and a _value_. You may access an _object_ of any type through a _value_ of type `char`. My first example accesses an _object_ of type `SomeType` through a _value_ of type `char` (OK). The second attempts to access an _object_ of type `char` through a _value_ of type `SomeType` (undefined). That is _not_ how `aligned_storage` works. To use `aligned_storage` you must construct a new _object_ in the storage previously occupied by the object of type `aligned_storage<>::type` using a placement-new expression. – Miles Budnek Aug 02 '19 at 05:45
  • @MilesBudnek thanks for your help, but I think we can stop there lol :) I maintain that _construct a new object in the storage_ doesn't lift magically the restrictions you want to grant to paragraph 8.2.1.11. Because `new` is not a construct of the abstract machine. It has no more privileges than raw access. And constructing an object, is writing stuff of a type T over the object-representation of that space, which should be UB by your interpretation as soon as we interpret that space like a T* (the return value of new). And when you program a `variant` class (I did) you may not... – v.oddou Aug 02 '19 at 06:12
  • ... keep around the return result of new, but simply cast your underlying storage when queried by clients through `get()`. And it's hard to prove that there is a difference in static analysis path by compilers between the two approaches. – v.oddou Aug 02 '19 at 06:13
  • _As an aside, paragraph 6.7.4 mentions the difference between value-representation and object-representation being the potential padding holes_ – v.oddou Aug 02 '19 at 06:15
  • @v.oddou What do you mean `new` doesn't have any more privileges? It has its own entire [section](https://timsong-cpp.github.io/cppwp/n4659/expr.new) of the standard! [\[basic.stc.dynamic\]](https://timsong-cpp.github.io/cppwp/n4659/basic.stc.dynamic#1) explicitly says _new-expressions_ create objects. Yes, you may cast a pointer to `aligned_storage<>::type`. Since C++17 doing so requires a `std::launder` though, since [no object of type `aligned_storage<>::type` exists there anymore](https://timsong-cpp.github.io/cppwp/n4659/basic.life#5). Its lifetime ended when the storage was reused. – Miles Budnek Aug 02 '19 at 06:20
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/197373/discussion-between-miles-budnek-and-v-oddou). – Miles Budnek Aug 02 '19 at 06:26
  • @MilesBudnek and v.oddou could one of you ,please summarize the conclusion of your discussions and chat? I am a bit lost in your back and forth, but I would like to include relevant parts of your discussion in the answer to the question. – Lawless Aug 02 '19 at 14:22
  • @Lawless I did that in the EDIT parts of my answer. The discussion mentions that prior to C++17 it was an active source of interpretation wars (not unlike the above comments. Problem also mentioned in Alf's answer https://stackoverflow.com/a/27492569/893406) But the spirit the committee wanted to convey has been clarified in C++17 by the notion of "lifetime" : basically it's impossible to interpret one memory space through 2 different types at the same time, by spirit of the new standard. The only possible escape for you is to use Jerry Coffin's solution; or a memcpy (`bit_cast` solution). – v.oddou Aug 02 '19 at 14:35
  • Another interesting sidepoint was that while analyzing how `variant` is implemented by gcc, Miles Budnek feels that the double static cast through a void* and to T* is edgy stuff, but maybe library implementors may ere on the side of UB as long as they know how gcc is implemented and what it will actually do. – v.oddou Aug 02 '19 at 14:38
  • Lastly, what I find fascinating is the introduction of a new language contraption called a `pointer optimization barrier` which is `std::launder`. That little facility looks like nothing much in cppreference, and I'll be sure to read more about it. But it is the same concept as load/store reordering prevention barriers for mutex. And it sounds like it's a big deal to me, especially since it sounds exactly like the primitive that would make all that char to T* aliasing possible, by breaking the compiler confidence in the up-to-date-ness of the involved registers, and force a reload. – v.oddou Aug 02 '19 at 14:48
  • @v.oddou I tried really hard to understand why the code that you posted above is illegal. Unfortunately I am not an expert in assembly. I would appreciate it if you give me a brief explanation. – Lawless Aug 02 '19 at 15:31
  • @Lawless well illegal doesn't mean doesn't work. I tested this code in MSVC and it does the expected thing. What illegal means is that we have no guarantee that an update of the compiler is going to preserve that nice behavior. Or e.g, building with clang for all we know. If you were to write this reinterpretation in assembly you'd have no such problem of illegality. The problem is related to the optimizations that the compilation to assembly are authorized to be undertaken. – v.oddou Aug 02 '19 at 15:43
  • @v.oddou I see. But won't this render reinterpret_cast completely useless? It cannot be a lifetime issue because you are using a pointer to pun. – Lawless Aug 02 '19 at 15:50
  • @Lawless yes with such rules reinterpret sounds useless. That's a good point. The lifetime in my example starts after `new` and ends at destruction `~D0()`. Maybe as long as you access `inst1` in read only, you'll get away fine ? I have no idea anymore honestly. This language is borked beyond recognition. – v.oddou Aug 02 '19 at 15:55
  • After looking at it a second time, I think my example is wrong. I have to permute the mutation to a different string, and the call to launder. Because after launder, the compiler may pre-execute the path that initializes the string to "stuff" and keep what inst1 points to as "stuff". launder forces a reload so it has to come after the assignment. Let me fix that. – v.oddou Aug 02 '19 at 16:05
  • It's not the string access that's the problem. Accessing a `Dynamic` via a pointer to `Dynamic` results in undefined behavior. `std::launder` cannot help you here, since there is no `Dynamic` object at that address for it to return a pointer to. There is no way for it to "work", since there's no defined correct behavior. It can "do what you want", but that's only by coincidence, and it could stop doing what you want for any reason at any time. – Miles Budnek Aug 02 '19 at 17:24
  • 1
    Re `reinterpret_cast` being useless: Its usefulness is limited. `reinterpert_cast`'s primary usefulness is to access the byte-representation of an object by casting a pointer to it to a pointer to `char`. – Miles Budnek Aug 02 '19 at 17:32
0

After reading the discussion in https://stackoverflow.com/a/57318684/2166857 and reading the source code for bit_cast and a lot of research online, I think I have found the safest solution to my problem. This only works if

1) The align and size of both types match

2) both types are trivially copyable (https://en.cppreference.com/w/cpp/named_req/TriviallyCopyable)

First define a memory type using aligned_storage

typedef std::aligned_storage<sizeof(Dynamic<true>),alignof(Dynamic<true>)>::type DynamicMem;

then introduce the variable of that type

DynamicMem dynamic_buff;

then use placement new to initiate the object to the primary class (in my case Dynamic)

new (&dynamic_buff) Dynamic<true>();

then whenever is needed use reinterpret_cast to define the object reference or the pointer associated with it in a scope

{
    Dynamic<true>* dyn_ptr_true=reinterpret_cast<Dynamic<true>*>(&dynamic_buff)
    // do some stuff with dyn_ptr_true
}

This solution by no means is perfect, but it does do the job for me. I do encourage everybody to read the thread https://stackoverflow.com/a/57318684/2166857 and follow the back and forth between @Miles_Budnek and @v.oddou. I certainly learned a lot from it.

Lawless
  • 75
  • 1
  • 5
  • Unfortunately, the Standard failed to specify that if a T& whose target can be accessed as type T is reinterpret-casted to U&, and throughout the lifetime of the converted reference the object is accessed exclusively through it, then the reference should be usable to access the object as type U even if types T and U would otherwise be incompatible. There was never any reason for non-obtuse compilers not to support such a construct, but the maintainers of clang and gcc use the fact that it's not required as an excuse not to support it, and block any effort to change the Standard to require it. – supercat Nov 30 '20 at 18:20
0

The only safe type punning method in standard C++ is via std::bit_cast. However that may involve a copy instead of treating the same memory representation as a different type if the compiler isn't able to optimize it. Besides, currently std::bit_cast is only supported by MSVC although in Clang you can use __builtin_bit_cast

Since you're using GCC you can use the attribute __may_alias__ to tell it that the aliasing is safe

template<int T>
struct Dynamic {};

template<>
struct Dynamic<true>
{
    uint32_t v;
} __attribute__((__may_alias__));

template<>
struct Dynamic<false>
{
    float v;
} __attribute__((__may_alias__));

float f(Dynamic<true>& d_true)
{
    auto& d_false = *reinterpret_cast<Dynamic<false>*>(&d_true);
    return d_false.v;
}

Clang and ICC also support that attribute. See demo on Godbolt, no warnings are given

phuclv
  • 37,963
  • 15
  • 156
  • 475