Discussion:
Union type punning in C++ redux
(too old to reply)
Daniel
2020-02-19 04:40:49 UTC
Permalink
I've been following the "Union type punning in C++" posts with some interest,
but not exactly sure of the conclusion.

Would the following be legal C++?

#include <string>
#include <new>

// pod type
struct A
{
uint8_t tag;
};

// non-pod
struct B
{
A a;
std::string s;

B(const std::string& s)
: a{1}, s(s)
{

}
};

struct C
{
A a;
double d;

C(double d)
: a{ 2 }, d(d)
{

}
};

class V
{
union
{
A a;
B b;
C c;
};

public:
V(const std::string& s)
{
::new(&b) B(s);
}
V(double d)
{
::new(&c) C(d);
}
~V()
{
switch (tag())
{
case 1:
b.~B();
break;
case 2:
c.~C();
break;
default:
break;
}
}
uint8_t tag() const
{
return a.tag;
}
};

What if B and C were defined through inheritance from A instead, i.e.

struct B : A
{
std::string s;

B(const std::string& s)
: A{1}, s(s)
{

}
};


struct C : A
{
double d;

C(double d)
: A{ 2 }, d(d)
{

}
};

Thanks,
Daniel
Pavel
2020-02-19 06:21:26 UTC
Permalink
Post by Daniel
I've been following the "Union type punning in C++" posts with some interest,
but not exactly sure of the conclusion.
Would the following be legal C++?
#include <string>
#include <new>
// pod type
struct A
{
uint8_t tag;
};
// non-pod
struct B
{
A a;
std::string s;
B(const std::string& s)
: a{1}, s(s)
{
}
};
struct C
{
A a;
double d;
C(double d)
: a{ 2 }, d(d)
{
}
};
class V
{
union
{
A a;
B b;
C c;
};
V(const std::string& s)
{
::new(&b) B(s);
}
V(double d)
{
::new(&c) C(d);
}
~V()
{
switch (tag())
{
b.~B();
break;
c.~C();
break;
break;
}
}
uint8_t tag() const
{
return a.tag;
}
};
I think no: it is not allowed to inspect a.tag regardless of the constructor
used to construct V because `tag' is not a part of the common initial sequence
of either V::a and V::b or of V::a and V::c (I think the common initial sequence
is empty in both cases).
Post by Daniel
What if B and C were defined through inheritance from A instead, i.e.
struct B : A
{
std::string s;
B(const std::string& s)
: A{1}, s(s)
{
}
};
struct C : A
{
double d;
C(double d)
: A{ 2 }, d(d)
{
}
};
I think no, for same reason.

I think changing implementation of tag() to either of { return b.tag; } or {
return c.tag; } would make the code valid (then of course having V::a member
would be unnecessary).
Post by Daniel
Thanks,
Daniel
FWIW,

-Pavel
Daniel
2020-02-19 14:09:57 UTC
Permalink
Post by Pavel
Post by Daniel
Would the following be legal C++?
snipped
I think no
Would the following (non-union) alternative be legal C++?

#include <string>
#include <new>
#include <algorithm>

enum class tag_type : uint8_t {b,c};

struct A
{
tag_type tag;
};

struct B : A
{
uint8_t extra;
uint64_t n;

B(uint64_t n)
: A{tag_type::b}, n(n)
{

}
};

struct C : A
{
double d;

C(double d)
: A{tag_type::c}, d(d)
{

}
};

class V
{
static constexpr size_t data_size = std::max(sizeof(B), sizeof(C));
static constexpr size_t data_align = std::max(alignof(B), alignof(C));

typedef typename std::aligned_storage<data_size, data_align>::type data_t;

data_t data_;

public:
V(uint8_t n)
{
::new(&data_) B(n);
}
V(double d)
{
::new(&data_) B(d);
}
~V()
{
switch (tag())
{
case tag_type::b:
reinterpret_cast<const B*>(&data_)->~B();
break;
case tag_type::c:
reinterpret_cast<const C*>(&data_)->~C();
break;
default:
break;
}
}
tag_type tag() const
{
return reinterpret_cast<const A*>(&data_)->tag;
}
};
Öö Tiib
2020-02-19 16:25:53 UTC
Permalink
Post by Daniel
Post by Pavel
Post by Daniel
Would the following be legal C++?
snipped
I think no
Would the following (non-union) alternative be legal C++?
No. You have wrong idea that base classes are better.

Resulting are not standard layout types. It is because
the requirement (in [class.prop]) "has all non-static data
members and bit-fields in the class and its base classes
first declared in the same class" is not fulfilled.

And so "common initial sequence" does not apply and
"address of class object is same as address of its first
non-static data member object" does not also apply.
Post by Daniel
#include <string>
#include <new>
#include <algorithm>
enum class tag_type : uint8_t {b,c};
struct A
{
tag_type tag;
};
struct B : A
{
uint8_t extra;
uint64_t n;
B(uint64_t n)
: A{tag_type::b}, n(n)
{
}
};
struct C : A
{
double d;
C(double d)
: A{tag_type::c}, d(d)
{
}
};
class V
{
static constexpr size_t data_size = std::max(sizeof(B), sizeof(C));
static constexpr size_t data_align = std::max(alignof(B), alignof(C));
typedef typename std::aligned_storage<data_size, data_align>::type data_t;
data_t data_;
V(uint8_t n)
{
::new(&data_) B(n);
}
V(double d)
{
::new(&data_) B(d);
}
~V()
{
switch (tag())
{
reinterpret_cast<const B*>(&data_)->~B();
break;
reinterpret_cast<const C*>(&data_)->~C();
break;
break;
}
}
tag_type tag() const
{
return reinterpret_cast<const A*>(&data_)->tag;
}
};
I suggest to get rid of base classes with data members
and have A as first data member. Then it is proper
approach.

Additional note:
When you want to have standard library classes as data members of
your standard layout classes then always static_assert in code
that these are:

static_assert(std::is_standard_layout<std::string>::value
, "This code needs std::string to be standard layout");

It is because standard does not require it and that can turn
your reinterpret_cast of pointer of object into pointer of its
first member into undefined behavior.
Daniel
2020-02-19 17:08:46 UTC
Permalink
Post by Daniel
Would the following (non-union) alternative be legal C++?
<snipped>
No.
<snipped>
That's very helpful, thanks.

For this exercise, the design goals are correctness, compactness (assume 10's
of millions of V's), and encapsulation of the B's and C's, in that order. For
the last example, assuming eight byte alignment, it would be desirable to have
sizeof(V) == 16.

To that end, my next question is, would this be legal C++:

#include <string>
#include <new>
#include <algorithm>
#include <cstring>

struct B
{
uint8_t tag;
uint8_t extra;
uint64_t n;

B(uint64_t n, uint8_t extra = 0)
: tag{1}, n(n), extra(extra)
{

}
};

struct C
{
uint8_t tag;
double d;

C(double d)
: tag{2}, d(d)
{

}
};

class V
{
static constexpr size_t data_size = std::max(sizeof(B), sizeof(C));
static constexpr size_t data_align = std::max(alignof(B), alignof(C));

typedef typename std::aligned_storage<data_size, data_align>::type data_t;

data_t data_;

public:
V(uint8_t n)
{
::new(&data_) B(n);
}
V(double d)
{
::new(&data_) B(d);
}
~V()
{
switch (tag())
{
case 1:
reinterpret_cast<const B*>(&data_)->~B();
break;
case 2:
reinterpret_cast<const C*>(&data_)->~C();
break;
default:
break;
}
}
uint8_t tag() const
{
uint8_t t;
std::memcpy(&t, &data_, sizeof(uint8_t));
return t;
}
};
When you want to have standard library classes as data members of
your standard layout classes then always static_assert in code
static_assert(std::is_standard_layout<std::string>::value
, "This code needs std::string to be standard layout");
It is because standard does not require it and that can turn
your reinterpret_cast of pointer of object into pointer of its
first member into undefined behavior.
Thanks for pointing that out. For my purposes, I do need to support

std::allocator_traits<Alloc>::pointer

including fancy pointers.

Daniel
Öö Tiib
2020-02-19 20:22:42 UTC
Permalink
Post by Daniel
Post by Daniel
Would the following (non-union) alternative be legal C++?
<snipped>
No.
<snipped>
That's very helpful, thanks.
For this exercise, the design goals are correctness, compactness (assume 10's
of millions of V's), and encapsulation of the B's and C's, in that order. For
the last example, assuming eight byte alignment, it would be desirable to have
sizeof(V) == 16.
#include <string>
#include <new>
#include <algorithm>
#include <cstring>
struct B
{
uint8_t tag;
uint8_t extra;
uint64_t n;
B(uint64_t n, uint8_t extra = 0)
: tag{1}, n(n), extra(extra)
{
}
};
struct C
{
uint8_t tag;
double d;
C(double d)
: tag{2}, d(d)
{
}
};
class V
{
static constexpr size_t data_size = std::max(sizeof(B), sizeof(C));
static constexpr size_t data_align = std::max(alignof(B), alignof(C));
typedef typename std::aligned_storage<data_size, data_align>::type data_t;
data_t data_;
V(uint8_t n)
{
::new(&data_) B(n);
}
V(double d)
{
::new(&data_) B(d);
}
~V()
{
switch (tag())
{
reinterpret_cast<const B*>(&data_)->~B();
Why const B* not B*?
Post by Daniel
break;
reinterpret_cast<const C*>(&data_)->~C();
break;
break;
}
}
uint8_t tag() const
{
uint8_t t;
std::memcpy(&t, &data_, sizeof(uint8_t));
return t;
}
};
Yes, the classes are standard layout and therefore in tag()
you could just return *reinterpret_cast<uint8_t const*>(&data_);
as well.
Post by Daniel
When you want to have standard library classes as data members of
your standard layout classes then always static_assert in code
static_assert(std::is_standard_layout<std::string>::value
, "This code needs std::string to be standard layout");
It is because standard does not require it and that can turn
your reinterpret_cast of pointer of object into pointer of its
first member into undefined behavior.
Thanks for pointing that out. For my purposes, I do need to support
std::allocator_traits<Alloc>::pointer
including fancy pointers.
Daniel
Yes, when you are unsure if certain member in your B or C
is standard layout or not then check std::is_standard_layout
about the member or about whole B or C.
Daniel
2020-02-19 20:59:29 UTC
Permalink
Post by Öö Tiib
Post by Daniel
~V()
{
switch (tag())
{
reinterpret_cast<const B*>(&data_)->~B();
break;
Why const B* not B*?
No reason. Copied that piece from code that accesses the object.
Post by Öö Tiib
Post by Daniel
reinterpret_cast<const C*>(&data_)->~C();
break;
break;
}
}
uint8_t tag() const
{
uint8_t t;
std::memcpy(&t, &data_, sizeof(uint8_t));
return t;
}
};
Yes, the classes are standard layout and therefore in tag()
you could just return *reinterpret_cast<uint8_t const*>(&data_);
as well.
Thanks! very much appreciate your feedback.

Daniel
Pavel
2020-02-20 06:08:44 UTC
Permalink
Post by Öö Tiib
Post by Daniel
Post by Daniel
Would the following (non-union) alternative be legal C++?
<snipped>
No.
<snipped>
That's very helpful, thanks.
For this exercise, the design goals are correctness, compactness (assume 10's
of millions of V's), and encapsulation of the B's and C's, in that order. For
the last example, assuming eight byte alignment, it would be desirable to have
sizeof(V) == 16.
#include <string>
#include <new>
#include <algorithm>
#include <cstring>
struct B
{
uint8_t tag;
uint8_t extra;
uint64_t n;
B(uint64_t n, uint8_t extra = 0)
: tag{1}, n(n), extra(extra)
{
}
};
struct C
{
uint8_t tag;
double d;
C(double d)
: tag{2}, d(d)
{
}
};
class V
{
static constexpr size_t data_size = std::max(sizeof(B), sizeof(C));
static constexpr size_t data_align = std::max(alignof(B), alignof(C));
typedef typename std::aligned_storage<data_size, data_align>::type data_t;
data_t data_;
V(uint8_t n)
{
::new(&data_) B(n);
}
V(double d)
{
::new(&data_) B(d);
}
~V()
{
switch (tag())
{
reinterpret_cast<const B*>(&data_)->~B();
Why const B* not B*?
Post by Daniel
break;
reinterpret_cast<const C*>(&data_)->~C();
break;
break;
}
}
uint8_t tag() const
{
uint8_t t;
std::memcpy(&t, &data_, sizeof(uint8_t));
return t;
}
};
Yes, the classes are standard layout and therefore in tag()
you could just return *reinterpret_cast<uint8_t const*>(&data_);
as well.
I do not think this code has specified behavior as I could not find in the
Standard any additional guarantee about reinterpret_cast results conditioned on
the involved types' being having standard-layout.

The only thing that seem to be guaranteed about reinterpret_cast (except for
some cases of its application to the pointer to an object of class derived from
the class to which cast is done or other way around where it is guaranteed to
behave like static_cast) is that under certain conditions reverse cast of the
pointer received from the direct cast yields the original value of the pointer.

In other words, I think the result of reinterpret_cast of a pointer (with the
exception mentioned above) can only be useful to cast it back to the pointer to
the object of its original type. The above code relies on de-referenced result
of reinterpreted_cast so I think its behavior is unspecified.
Post by Öö Tiib
Post by Daniel
When you want to have standard library classes as data members of
your standard layout classes then always static_assert in code
static_assert(std::is_standard_layout<std::string>::value
, "This code needs std::string to be standard layout");
It is because standard does not require it and that can turn
your reinterpret_cast of pointer of object into pointer of its
first member into undefined behavior.
Thanks for pointing that out. For my purposes, I do need to support
std::allocator_traits<Alloc>::pointer
including fancy pointers.
Daniel
Yes, when you are unsure if certain member in your B or C
is standard layout or not then check std::is_standard_layout
about the member or about whole B or C.
HTH
-Pavel
Öö Tiib
2020-02-20 07:00:33 UTC
Permalink
Post by Pavel
Post by Öö Tiib
Post by Daniel
Post by Daniel
Would the following (non-union) alternative be legal C++?
<snipped>
No.
<snipped>
That's very helpful, thanks.
For this exercise, the design goals are correctness, compactness (assume 10's
of millions of V's), and encapsulation of the B's and C's, in that order. For
the last example, assuming eight byte alignment, it would be desirable to have
sizeof(V) == 16.
#include <string>
#include <new>
#include <algorithm>
#include <cstring>
struct B
{
uint8_t tag;
uint8_t extra;
uint64_t n;
B(uint64_t n, uint8_t extra = 0)
: tag{1}, n(n), extra(extra)
{
}
};
struct C
{
uint8_t tag;
double d;
C(double d)
: tag{2}, d(d)
{
}
};
class V
{
static constexpr size_t data_size = std::max(sizeof(B), sizeof(C));
static constexpr size_t data_align = std::max(alignof(B), alignof(C));
typedef typename std::aligned_storage<data_size, data_align>::type data_t;
data_t data_;
V(uint8_t n)
{
::new(&data_) B(n);
}
V(double d)
{
::new(&data_) B(d);
}
~V()
{
switch (tag())
{
reinterpret_cast<const B*>(&data_)->~B();
Why const B* not B*?
Post by Daniel
break;
reinterpret_cast<const C*>(&data_)->~C();
break;
break;
}
}
uint8_t tag() const
{
uint8_t t;
std::memcpy(&t, &data_, sizeof(uint8_t));
return t;
}
};
Yes, the classes are standard layout and therefore in tag()
you could just return *reinterpret_cast<uint8_t const*>(&data_);
as well.
I do not think this code has specified behavior as I could not find in the
Standard any additional guarantee about reinterpret_cast results conditioned on
the involved types' being having standard-layout.
I copy-paste from C++2017:

| Two objects a and b are pointer-interconvertible if:
| —(4.1) they are the same object, or
| —(4.2) one is a standard-layout union object and the other is a non-static data member of that object, or
| —(4.3) one is a standard-layout class object and the other is the first non-static data member of that object, or, if the object has no non-static data members, the first base class subobject of that object, or
| —(4.4) there exists an object c such that a and c are pointer-interconvertible, and c and b are pointerinterconvertible.
|
| If two objects are pointer-interconvertible, then they have the same address, and it is possible to obtain a pointer to one from a pointer to the other via a reinterpret_cast
Post by Pavel
The only thing that seem to be guaranteed about reinterpret_cast (except for
some cases of its application to the pointer to an object of class derived from
the class to which cast is done or other way around where it is guaranteed to
behave like static_cast) is that under certain conditions reverse cast of the
pointer received from the direct cast yields the original value of the pointer.
In other words, I think the result of reinterpret_cast of a pointer (with the
exception mentioned above) can only be useful to cast it back to the pointer to
the object of its original type. The above code relies on de-referenced result
of reinterpreted_cast so I think its behavior is unspecified.
I am quite certain that the reinterpret_cast is not so useless as you put it.
Post by Pavel
Post by Öö Tiib
Post by Daniel
When you want to have standard library classes as data members of
your standard layout classes then always static_assert in code
static_assert(std::is_standard_layout<std::string>::value
, "This code needs std::string to be standard layout");
It is because standard does not require it and that can turn
your reinterpret_cast of pointer of object into pointer of its
first member into undefined behavior.
Thanks for pointing that out. For my purposes, I do need to support
std::allocator_traits<Alloc>::pointer
including fancy pointers.
Daniel
Yes, when you are unsure if certain member in your B or C
is standard layout or not then check std::is_standard_layout
about the member or about whole B or C.
HTH
-Pavel
Pavel
2020-02-21 07:03:07 UTC
Permalink
Post by Öö Tiib
Post by Pavel
Post by Öö Tiib
Post by Daniel
Post by Daniel
Would the following (non-union) alternative be legal C++?
<snipped>
No.
<snipped>
That's very helpful, thanks.
For this exercise, the design goals are correctness, compactness (assume 10's
of millions of V's), and encapsulation of the B's and C's, in that order. For
the last example, assuming eight byte alignment, it would be desirable to have
sizeof(V) == 16.
#include <string>
#include <new>
#include <algorithm>
#include <cstring>
struct B
{
uint8_t tag;
uint8_t extra;
uint64_t n;
B(uint64_t n, uint8_t extra = 0)
: tag{1}, n(n), extra(extra)
{
}
};
struct C
{
uint8_t tag;
double d;
C(double d)
: tag{2}, d(d)
{
}
};
class V
{
static constexpr size_t data_size = std::max(sizeof(B), sizeof(C));
static constexpr size_t data_align = std::max(alignof(B), alignof(C));
typedef typename std::aligned_storage<data_size, data_align>::type data_t;
data_t data_;
V(uint8_t n)
{
::new(&data_) B(n);
}
V(double d)
{
::new(&data_) B(d);
}
~V()
{
switch (tag())
{
reinterpret_cast<const B*>(&data_)->~B();
Why const B* not B*?
Post by Daniel
break;
reinterpret_cast<const C*>(&data_)->~C();
break;
break;
}
}
uint8_t tag() const
{
uint8_t t;
std::memcpy(&t, &data_, sizeof(uint8_t));
return t;
}
};
Yes, the classes are standard layout and therefore in tag()
you could just return *reinterpret_cast<uint8_t const*>(&data_);
as well.
I do not think this code has specified behavior as I could not find in the
Standard any additional guarantee about reinterpret_cast results conditioned on
the involved types' being having standard-layout.
| —(4.1) they are the same object, or
| —(4.2) one is a standard-layout union object and the other is a non-static data member of that object, or
| —(4.3) one is a standard-layout class object and the other is the first non-static data member of that object, or, if the object has no non-static data members, the first
BTW. 2020 draft n4849 changes "first" to "any" here
Post by Öö Tiib
base class subobject of that object, or
| —(4.4) there exists an object c such that a and c are pointer-interconvertible, and c and b are pointerinterconvertible.
|
| If two objects are pointer-interconvertible, then they have the same address, and it is possible to obtain a pointer to one from a pointer to the other via a reinterpret_cast
Thanks, I missed this language. I think the only non-trivial addition to my
interpretation of the standard it brings is 4.3 (the guarantees given by 4.2 can
be taken advantage of with unions and no reinterpret_cast and the other 2 are
trivial); but I don't think it helps the legality of the latest OP's code
because it is not obvious to me which of these 4 rules can be applied to
conclude that &V::data_ and the results of its reinterpret-casting to const A*
or const B* are pointer-interconvertible.
Post by Öö Tiib
Post by Pavel
The only thing that seem to be guaranteed about reinterpret_cast (except for
some cases of its application to the pointer to an object of class derived from
the class to which cast is done or other way around where it is guaranteed to
behave like static_cast) is that under certain conditions reverse cast of the
pointer received from the direct cast yields the original value of the pointer.
In other words, I think the result of reinterpret_cast of a pointer (with the
exception mentioned above) can only be useful to cast it back to the pointer to
the object of its original type. The above code relies on de-referenced result
of reinterpreted_cast so I think its behavior is unspecified.
I am quite certain that the reinterpret_cast is not so useless as you put it.
Well, most of legal and useful uses I can remember were casting from void* to T*
and back; but your citation (specifically 4.3) does add one more potentially
useful legal use I do not remember using (but now that you mentioned it I think
I am recalling seeing it somewhere), namely the casts between a pointer to a
standard-layout class object and the pointer to its first member.

(It also says that for an "empty" standard-layout class object we can
reinterpret_cast between that object and any of its base objects (obviously,
also empty) -- but that is not very useful as it is both shorter and IMHO more
readable to just use static_cast for that).
Post by Öö Tiib
Post by Pavel
Post by Öö Tiib
Post by Daniel
When you want to have standard library classes as data members of
your standard layout classes then always static_assert in code
static_assert(std::is_standard_layout<std::string>::value
, "This code needs std::string to be standard layout");
It is because standard does not require it and that can turn
your reinterpret_cast of pointer of object into pointer of its
first member into undefined behavior.
Thanks for pointing that out. For my purposes, I do need to support
std::allocator_traits<Alloc>::pointer
including fancy pointers.
Daniel
Yes, when you are unsure if certain member in your B or C
is standard layout or not then check std::is_standard_layout
about the member or about whole B or C.
HTH
-Pavel
Daniel
2020-02-21 14:05:23 UTC
Permalink
Post by Pavel
I am quite certain that the reinterpret_cast is not so useless as you put >> it.
Well, most of legal and useful uses I can remember were casting from void*
to T* and back; but your citation (specifically 4.3) does add one more
potentially useful legal use I do not remember using (but now that you
mentioned it I think I am recalling seeing it somewhere), namely the casts
between a pointer to a standard-layout class object and the pointer to its
first member.
Hardly normative, but by my count, boost 1_71 has 4414 occurrences of
reinterpret_cast in 268 header files, including many uses with aligned
storage.
Pavel
2020-02-23 07:04:04 UTC
Permalink
Post by Daniel
Post by Pavel
I am quite certain that the reinterpret_cast is not so useless as you put >> it.
Well, most of legal and useful uses I can remember were casting from void*
to T* and back; but your citation (specifically 4.3) does add one more
potentially useful legal use I do not remember using (but now that you
mentioned it I think I am recalling seeing it somewhere), namely the casts
between a pointer to a standard-layout class object and the pointer to its
first member.
Hardly normative, but by my count, boost 1_71 has 4414 occurrences of
reinterpret_cast in 268 header files, including many uses with aligned
storage.
We could discuss few specific ones that you believe are most common or useful
and try to figure out whether they are legal C++ and, if not, how they can be
fixed.

As for your last example, the following seems to be a variant class that has
same public API as yours but is IMHO legal C++ (I omitted the definitions of B
and C as they did not need any changes):

class V2 {
union {
B b;
C c;
};
public:
V2(uint64_t n): b(n) { }
V2(double d): c(d) { }
~V2() {
switch (tag()) {
case 1:
b.~B();
break;
case 2:
c.~C();
break;
default:
break;
}
}
uint8_t tag() const { return b.tag; }
};

Above, it is legal to call access `b.tag' regardless of which of b or c is the
active member of the union because `tag' is a part of common initial sequence of
B and C as defined in 11.4-25 of the Standard.

V2 also fulfills your requirement of having sizeof of 16 assuming 8-bytes alignment.
Daniel
2020-02-23 15:46:56 UTC
Permalink
Post by Pavel
As for your last example, the following seems to be a variant class that has
same public API as yours but is IMHO legal C++ (I omitted the definitions of
class V2 {
union {
B b;
C c;
};
V2(uint64_t n): b(n) { }
V2(double d): c(d) { }
~V2() {
switch (tag()) {
b.~B();
break;
c.~C();
break;
break;
}
}
uint8_t tag() const { return b.tag; }
};
Above, it is legal to call access `b.tag' regardless of which of b or c is
the active member of the union because `tag' is a part of common initial
sequence of B and C as defined in 11.4-25 of the Standard.
V2 also fulfills your requirement of having sizeof of 16 assuming 8-bytes alignment.
That would do, thanks,
Daniel
Öö Tiib
2020-02-21 18:26:50 UTC
Permalink
Post by Pavel
Post by Öö Tiib
Post by Pavel
Post by Öö Tiib
Post by Daniel
Post by Daniel
Would the following (non-union) alternative be legal C++?
<snipped>
No.
<snipped>
That's very helpful, thanks.
For this exercise, the design goals are correctness, compactness (assume 10's
of millions of V's), and encapsulation of the B's and C's, in that order. For
the last example, assuming eight byte alignment, it would be desirable to have
sizeof(V) == 16.
#include <string>
#include <new>
#include <algorithm>
#include <cstring>
struct B
{
uint8_t tag;
uint8_t extra;
uint64_t n;
B(uint64_t n, uint8_t extra = 0)
: tag{1}, n(n), extra(extra)
{
}
};
struct C
{
uint8_t tag;
double d;
C(double d)
: tag{2}, d(d)
{
}
};
class V
{
static constexpr size_t data_size = std::max(sizeof(B), sizeof(C));
static constexpr size_t data_align = std::max(alignof(B), alignof(C));
typedef typename std::aligned_storage<data_size, data_align>::type data_t;
data_t data_;
V(uint8_t n)
{
::new(&data_) B(n);
}
V(double d)
{
::new(&data_) B(d);
}
~V()
{
switch (tag())
{
reinterpret_cast<const B*>(&data_)->~B();
Why const B* not B*?
Post by Daniel
break;
reinterpret_cast<const C*>(&data_)->~C();
break;
break;
}
}
uint8_t tag() const
{
uint8_t t;
std::memcpy(&t, &data_, sizeof(uint8_t));
return t;
}
};
Yes, the classes are standard layout and therefore in tag()
you could just return *reinterpret_cast<uint8_t const*>(&data_);
as well.
I do not think this code has specified behavior as I could not find in the
Standard any additional guarantee about reinterpret_cast results conditioned on
the involved types' being having standard-layout.
| —(4.1) they are the same object, or
| —(4.2) one is a standard-layout union object and the other is a non-static data member of that object, or
| —(4.3) one is a standard-layout class object and the other is the first non-static data member of that object, or, if the object has no non-static data members, the first
BTW. 2020 draft n4849 changes "first" to "any" here
Post by Öö Tiib
base class subobject of that object, or
| —(4.4) there exists an object c such that a and c are pointer-interconvertible, and c and b are pointerinterconvertible.
|
| If two objects are pointer-interconvertible, then they have the same address, and it is possible to obtain a pointer to one from a pointer to the other via a reinterpret_cast
Thanks, I missed this language. I think the only non-trivial addition to my
interpretation of the standard it brings is 4.3 (the guarantees given by 4.2 can
be taken advantage of with unions and no reinterpret_cast and the other 2 are
trivial); but I don't think it helps the legality of the latest OP's code
because it is not obvious to me which of these 4 rules can be applied to
conclude that &V::data_ and the results of its reinterpret-casting to const A*
or const B* are pointer-interconvertible.
You totally misinterpreted extent of my example. It was only given
to show that "I could not find in the Standard any additional guarantee
about reinterpret_cast results conditioned on the involved types' being
having standard-layout" does not matter since I could. Search bit more.
Post by Pavel
Post by Öö Tiib
Post by Pavel
The only thing that seem to be guaranteed about reinterpret_cast (except for
some cases of its application to the pointer to an object of class derived from
the class to which cast is done or other way around where it is guaranteed to
behave like static_cast) is that under certain conditions reverse cast of the
pointer received from the direct cast yields the original value of the pointer.
In other words, I think the result of reinterpret_cast of a pointer (with the
exception mentioned above) can only be useful to cast it back to the pointer to
the object of its original type. The above code relies on de-referenced result
of reinterpreted_cast so I think its behavior is unspecified.
I am quite certain that the reinterpret_cast is not so useless as you put it.
Well, most of legal and useful uses I can remember were casting from void* to T*
and back; but your citation (specifically 4.3) does add one more potentially
useful legal use I do not remember using (but now that you mentioned it I think
I am recalling seeing it somewhere), namely the casts between a pointer to a
standard-layout class object and the pointer to its first member.
(It also says that for an "empty" standard-layout class object we can
reinterpret_cast between that object and any of its base objects (obviously,
also empty) -- but that is not very useful as it is both shorter and IMHO more
readable to just use static_cast for that).
Wrong place, red herring. That is what is fun about standard, there are additional guarantees about bytes (IOW chars) and also about that
std::aligned_storage. ;)
Post by Pavel
Post by Öö Tiib
Post by Pavel
Post by Öö Tiib
Post by Daniel
When you want to have standard library classes as data members of
your standard layout classes then always static_assert in code
static_assert(std::is_standard_layout<std::string>::value
, "This code needs std::string to be standard layout");
It is because standard does not require it and that can turn
your reinterpret_cast of pointer of object into pointer of its
first member into undefined behavior.
Thanks for pointing that out. For my purposes, I do need to support
std::allocator_traits<Alloc>::pointer
including fancy pointers.
Daniel
Yes, when you are unsure if certain member in your B or C
is standard layout or not then check std::is_standard_layout
about the member or about whole B or C.
HTH
-Pavel
Pavel
2020-02-23 05:13:26 UTC
Permalink
Post by Öö Tiib
Post by Pavel
Post by Öö Tiib
Post by Pavel
Post by Öö Tiib
Post by Daniel
Post by Daniel
Would the following (non-union) alternative be legal C++?
<snipped>
No.
<snipped>
That's very helpful, thanks.
For this exercise, the design goals are correctness, compactness (assume 10's
of millions of V's), and encapsulation of the B's and C's, in that order. For
the last example, assuming eight byte alignment, it would be desirable to have
sizeof(V) == 16.
#include <string>
#include <new>
#include <algorithm>
#include <cstring>
struct B
{
uint8_t tag;
uint8_t extra;
uint64_t n;
B(uint64_t n, uint8_t extra = 0)
: tag{1}, n(n), extra(extra)
{
}
};
struct C
{
uint8_t tag;
double d;
C(double d)
: tag{2}, d(d)
{
}
};
class V
{
static constexpr size_t data_size = std::max(sizeof(B), sizeof(C));
static constexpr size_t data_align = std::max(alignof(B), alignof(C));
typedef typename std::aligned_storage<data_size, data_align>::type data_t;
data_t data_;
V(uint8_t n)
{
::new(&data_) B(n);
}
V(double d)
{
::new(&data_) B(d);
}
~V()
{
switch (tag())
{
reinterpret_cast<const B*>(&data_)->~B();
Why const B* not B*?
Post by Daniel
break;
reinterpret_cast<const C*>(&data_)->~C();
break;
break;
}
}
uint8_t tag() const
{
uint8_t t;
std::memcpy(&t, &data_, sizeof(uint8_t));
return t;
}
};
Yes, the classes are standard layout and therefore in tag()
you could just return *reinterpret_cast<uint8_t const*>(&data_);
as well.
I do not think this code has specified behavior as I could not find in the
Standard any additional guarantee about reinterpret_cast results conditioned on
the involved types' being having standard-layout.
| —(4.1) they are the same object, or
| —(4.2) one is a standard-layout union object and the other is a non-static data member of that object, or
| —(4.3) one is a standard-layout class object and the other is the first non-static data member of that object, or, if the object has no non-static data members, the first
BTW. 2020 draft n4849 changes "first" to "any" here
Post by Öö Tiib
base class subobject of that object, or
| —(4.4) there exists an object c such that a and c are pointer-interconvertible, and c and b are pointerinterconvertible.
|
| If two objects are pointer-interconvertible, then they have the same address, and it is possible to obtain a pointer to one from a pointer to the other via a reinterpret_cast
Thanks, I missed this language. I think the only non-trivial addition to my
interpretation of the standard it brings is 4.3 (the guarantees given by 4.2 can
be taken advantage of with unions and no reinterpret_cast and the other 2 are
trivial); but I don't think it helps the legality of the latest OP's code
because it is not obvious to me which of these 4 rules can be applied to
conclude that &V::data_ and the results of its reinterpret-casting to const A*
or const B* are pointer-interconvertible.
You totally misinterpreted extent of my example. It was only given
to show that "I could not find in the Standard any additional guarantee
about reinterpret_cast results conditioned on the involved types' being
having standard-layout" does not matter since I could.
I could not possibly "misinterpret the extent" of your example because my answer
to your answer did not try to interpret its extent. All my answer to your answer
said was:

1. That your answer pointed out one legal and potentially useful use of
reinterpret_cast that I did not know before.
2. That your answer did not support your opinion of the legality of the latest
code of the OP.

Search bit more.
Thank you, I did and still did not see anything that would help in proving that
the reinterpret_casts used in the OP's last version of the code were legal. Did you?
Post by Öö Tiib
Post by Pavel
Post by Öö Tiib
Post by Pavel
The only thing that seem to be guaranteed about reinterpret_cast (except for
some cases of its application to the pointer to an object of class derived from
the class to which cast is done or other way around where it is guaranteed to
behave like static_cast) is that under certain conditions reverse cast of the
pointer received from the direct cast yields the original value of the pointer.
In other words, I think the result of reinterpret_cast of a pointer (with the
exception mentioned above) can only be useful to cast it back to the pointer to
the object of its original type. The above code relies on de-referenced result
of reinterpreted_cast so I think its behavior is unspecified.
I am quite certain that the reinterpret_cast is not so useless as you put it.
Well, most of legal and useful uses I can remember were casting from void* to T*
and back; but your citation (specifically 4.3) does add one more potentially
useful legal use I do not remember using (but now that you mentioned it I think
I am recalling seeing it somewhere), namely the casts between a pointer to a
standard-layout class object and the pointer to its first member.
(It also says that for an "empty" standard-layout class object we can
reinterpret_cast between that object and any of its base objects (obviously,
also empty) -- but that is not very useful as it is both shorter and IMHO more
readable to just use static_cast for that).
Wrong place, red herring.
??
Post by Öö Tiib
That is what is fun about standard, there are additional guarantees about bytes (IOW chars) and also about that
std::aligned_storage. ;)
There is nothing special I can see about std::aligned_storage with regard to
reinterpret_cast. That said, I am always glad to learn from people who are know
more than I do. ;)
Post by Öö Tiib
Post by Pavel
Post by Öö Tiib
Post by Pavel
Post by Öö Tiib
Post by Daniel
When you want to have standard library classes as data members of
your standard layout classes then always static_assert in code
static_assert(std::is_standard_layout<std::string>::value
, "This code needs std::string to be standard layout");
It is because standard does not require it and that can turn
your reinterpret_cast of pointer of object into pointer of its
first member into undefined behavior.
Thanks for pointing that out. For my purposes, I do need to support
std::allocator_traits<Alloc>::pointer
including fancy pointers.
Daniel
Yes, when you are unsure if certain member in your B or C
is standard layout or not then check std::is_standard_layout
about the member or about whole B or C.
HTH
-Pavel
Daniel
2020-02-23 15:43:40 UTC
Permalink
Post by Pavel
Post by Öö Tiib
std::aligned_storage. ;)
There is nothing special I can see about std::aligned_storage with regard to
reinterpret_cast.
Every use I've seen of std::aligned_storage has reinterpret_cast, without
reinterpret_cast, what could you do with it? For instance, the example in
cppreference,

template<class T, std::size_t N>
class static_vector
{
// properly aligned uninitialized storage for N T's
typename std::aligned_storage<sizeof(T), alignof(T)>::type data[N];
std::size_t m_size = 0;

public:
// Create an object in aligned storage
template<typename ...Args> void emplace_back(Args&&... args)
{
if( m_size >= N ) // possible error handling
throw std::bad_alloc{};

// construct value in memory of aligned storage
// using inplace operator new
new(&data[m_size]) T(std::forward<Args>(args)...);
++m_size;
}

// Access an object in aligned storage
const T& operator[](std::size_t pos) const
{
// note: needs std::launder as of C++17
return *reinterpret_cast<const T*>(&data[pos]);
}

// Delete objects from aligned storage
~static_vector()
{
for(std::size_t pos = 0; pos < m_size; ++pos) {
// note: needs std::launder as of C++17
reinterpret_cast<T*>(&data[pos])->~T();
}
}
};

I note the comment in the destructor, "needs std::launder as of C++17",
which I wasn't previously aware of.

Daniel
Pavel
2020-02-24 06:09:22 UTC
Permalink
Post by Daniel
Post by Pavel
Post by Öö Tiib
std::aligned_storage. ;)
There is nothing special I can see about std::aligned_storage with regard to
reinterpret_cast.
Every use I've seen of std::aligned_storage has reinterpret_cast, without
reinterpret_cast, what could you do with it?
It is difficult to say. Somehow the Standard carefully avoids ever guaranteeing
anything about the result of reinterpret_cast<T1*>(tPtr); where tPtr is of type
T* and T is different from T1.

E.g. looking at 7.6.1.9 "Renterpret Cast" in n4849 (and, I believe all previous
Standards):

3 [Note: The mapping performed by reinterpret_cast might, or might not, produce
a representation different from the original value. — end note]
...

You would think that somewhere in

7 An object pointer can be explicitly converted to an object pointer of a
different type... When a prvalue v of object pointer type is converted to the
object pointer type “pointer to cv T”, the result is static_cast<cv
T*>(static_cast<cv void*>(v)). [Note: Converting a prvalue of type “pointer to
T1” to the type “pointer
to T2” (where T1 and T2 are object types and where the alignment requirements of
T2 are no stricter than
those of T1) and back to its original type yields the original pointer value. —
end note]

the Standard would guarantee something about memory addresses like "the address
of v and the address represented by the result of reinterpret_cast<T2*>(v) are
same" but alas. The guarantees are only for some two-way applications e.g. T* to
intptr_t (see 7.6.1.9-5) and back or T1 to T2 and back.

Of course, everybody uses static_cast and reinterpret_cast for this purpose so
it probably "will just work". Wherever possible I would, however, stick to union
approach which seems to be better defined.
Post by Daniel
For instance, the example in
cppreference,
template<class T, std::size_t N>
class static_vector
{
// properly aligned uninitialized storage for N T's
typename std::aligned_storage<sizeof(T), alignof(T)>::type data[N];
std::size_t m_size = 0;
// Create an object in aligned storage
template<typename ...Args> void emplace_back(Args&&... args)
{
if( m_size >= N ) // possible error handling
throw std::bad_alloc{};
// construct value in memory of aligned storage
// using inplace operator new
new(&data[m_size]) T(std::forward<Args>(args)...);
++m_size;
}
// Access an object in aligned storage
const T& operator[](std::size_t pos) const
{
// note: needs std::launder as of C++17
return *reinterpret_cast<const T*>(&data[pos]);
}
// Delete objects from aligned storage
~static_vector()
{
for(std::size_t pos = 0; pos < m_size; ++pos) {
// note: needs std::launder as of C++17
reinterpret_cast<T*>(&data[pos])->~T();
}
}
};
I note the comment in the destructor, "needs std::launder as of C++17",
which I wasn't previously aware of.
It is probably prudent to use it even though it may be important only when T has
some special features, see 17.6.4 "Pointer optimization barrier":

5 [Note: If a new object is created in storage occupied by an existing object of
the same type, a pointer
to the original object can be used to refer to the new object unless its
complete object is a const object
or it is a base class subobject; in the latter cases, this function can be used
to obtain a usable pointer
to the new object. ...]
Post by Daniel
Daniel
Öö Tiib
2020-02-24 07:37:56 UTC
Permalink
Post by Pavel
Post by Daniel
Post by Pavel
Post by Öö Tiib
std::aligned_storage. ;)
There is nothing special I can see about std::aligned_storage with regard to
reinterpret_cast.
Every use I've seen of std::aligned_storage has reinterpret_cast, without
reinterpret_cast, what could you do with it?
It is difficult to say. Somehow the Standard carefully avoids ever guaranteeing
anything about the result of reinterpret_cast<T1*>(tPtr); where tPtr is of type
T* and T is different from T1.
E.g. looking at 7.6.1.9 "Renterpret Cast" in n4849 (and, I believe all previous
3 [Note: The mapping performed by reinterpret_cast might, or might not, produce
a representation different from the original value. — end note]
...
You would think that somewhere in
7 An object pointer can be explicitly converted to an object pointer of a
different type... When a prvalue v of object pointer type is converted to the
object pointer type “pointer to cv T”, the result is static_cast<cv
T*>(static_cast<cv void*>(v)). [Note: Converting a prvalue of type “pointer to
T1” to the type “pointer
to T2” (where T1 and T2 are object types and where the alignment requirements of
T2 are no stricter than
those of T1) and back to its original type yields the original pointer value. —
end note]
the Standard would guarantee something about memory addresses like "the address
of v and the address represented by the result of reinterpret_cast<T2*>(v) are
same" but alas. The guarantees are only for some two-way applications e.g. T* to
intptr_t (see 7.6.1.9-5) and back or T1 to T2 and back.
Of course, everybody uses static_cast and reinterpret_cast for this purpose so
it probably "will just work". Wherever possible I would, however, stick to union
approach which seems to be better defined.
So you could find place where standard says that reinterpret_cast<T*>(v)
is same as static_cast<T*>(static_cast<void*>(v))?

May be you cant find that?:
A prvalue of type “pointer to cv1 void” can be converted to a prvalue of type “pointer to cv2 T”, where T is an object type and cv2 is the same cv-qualification as, or greater cv-qualification than, cv1. If the original pointer value represents the address A of a byte in memory and A does not satisfy the alignment requirement of T, then the resulting pointer value is unspecified. Otherwise, if the original pointer value points to an object a, and there is an object b of type T (ignoring cv-qualification) that is pointer-interconvertible (6.9.2) with a, the result is a pointer to b. Otherwise, the pointer value is unchanged by the conversion.

About first member of standard layout struct being pointer-interconvertible
to struct itself I already copy pasted. And aligned_storage
is satisfying the alignment requirements of T so why you deny
that Daniel's example is well-formed?
Post by Pavel
Post by Daniel
For instance, the example in
cppreference,
template<class T, std::size_t N>
class static_vector
{
// properly aligned uninitialized storage for N T's
typename std::aligned_storage<sizeof(T), alignof(T)>::type data[N];
std::size_t m_size = 0;
// Create an object in aligned storage
template<typename ...Args> void emplace_back(Args&&... args)
{
if( m_size >= N ) // possible error handling
throw std::bad_alloc{};
// construct value in memory of aligned storage
// using inplace operator new
new(&data[m_size]) T(std::forward<Args>(args)...);
++m_size;
}
// Access an object in aligned storage
const T& operator[](std::size_t pos) const
{
// note: needs std::launder as of C++17
return *reinterpret_cast<const T*>(&data[pos]);
}
// Delete objects from aligned storage
~static_vector()
{
for(std::size_t pos = 0; pos < m_size; ++pos) {
// note: needs std::launder as of C++17
reinterpret_cast<T*>(&data[pos])->~T();
}
}
};
I note the comment in the destructor, "needs std::launder as of C++17",
which I wasn't previously aware of.
It is probably prudent to use it even though it may be important only when T has
5 [Note: If a new object is created in storage occupied by an existing object of
the same type, a pointer
to the original object can be used to refer to the new object unless its
complete object is a const object
or it is a base class subobject; in the latter cases, this function can be used
to obtain a usable pointer
to the new object. ...]
Post by Daniel
Daniel
Öö Tiib
2020-02-24 07:43:37 UTC
Permalink
Post by Öö Tiib
About first member of standard layout struct being pointer-interconvertible
to struct itself I already copy pasted. And aligned_storage
is satisfying the alignment requirements of T so why you deny
that Daniel's example is well-formed?
I meant well-defined.
Pavel
2020-02-25 05:03:59 UTC
Permalink
Post by Öö Tiib
Post by Pavel
Post by Daniel
Post by Pavel
Post by Öö Tiib
std::aligned_storage. ;)
There is nothing special I can see about std::aligned_storage with regard to
reinterpret_cast.
Every use I've seen of std::aligned_storage has reinterpret_cast, without
reinterpret_cast, what could you do with it?
It is difficult to say. Somehow the Standard carefully avoids ever guaranteeing
anything about the result of reinterpret_cast<T1*>(tPtr); where tPtr is of type
T* and T is different from T1.
E.g. looking at 7.6.1.9 "Renterpret Cast" in n4849 (and, I believe all previous
3 [Note: The mapping performed by reinterpret_cast might, or might not, produce
a representation different from the original value. — end note]
...
You would think that somewhere in
7 An object pointer can be explicitly converted to an object pointer of a
different type... When a prvalue v of object pointer type is converted to the
object pointer type “pointer to cv T”, the result is static_cast<cv
T*>(static_cast<cv void*>(v)). [Note: Converting a prvalue of type “pointer to
T1” to the type “pointer
to T2” (where T1 and T2 are object types and where the alignment requirements of
T2 are no stricter than
those of T1) and back to its original type yields the original pointer value. —
end note]
the Standard would guarantee something about memory addresses like "the address
of v and the address represented by the result of reinterpret_cast<T2*>(v) are
same" but alas. The guarantees are only for some two-way applications e.g. T* to
intptr_t (see 7.6.1.9-5) and back or T1 to T2 and back.
Of course, everybody uses static_cast and reinterpret_cast for this purpose so
it probably "will just work". Wherever possible I would, however, stick to union
approach which seems to be better defined.
So you could find place where standard says that reinterpret_cast<T*>(v)
is same as static_cast<T*>(static_cast<void*>(v))?
A prvalue of type “pointer to cv1 void” can be converted to a prvalue of type “pointer to cv2 T”, where T is an object type and cv2 is the same cv-qualification as, or greater cv-qualification than, cv1. If the original pointer value represents the address A of a byte in memory and A does not satisfy the alignment requirement of T, then the resulting pointer value is unspecified. Otherwise, if the original pointer value points to an object a, and there is an object b of type T (ignoring cv-qualification) that is pointer-interconvertible (6.9.2) with a, the result is a pointer to b. Otherwise, the pointer value is unchanged by the conversion.
About first member of standard layout struct being pointer-interconvertible
to struct itself I already copy pasted. And aligned_storage
is satisfying the alignment requirements of T so why you deny
that Daniel's example is well-formed?
Because the above text requires that "there is an object b of type T (ignoring
cv-qualification) that is pointer-interconvertible (6.9.2) with a" for result to
be a pointer to b.

How can we deduce that the type std::aligned_storage<T,
some-align-greater-than-or-equal-to-alignof<T> >::type is
pointer-interconvertible with T (even ignoring cv-qualification)?
Post by Öö Tiib
Post by Pavel
Post by Daniel
For instance, the example in
cppreference,
template<class T, std::size_t N>
class static_vector
{
// properly aligned uninitialized storage for N T's
typename std::aligned_storage<sizeof(T), alignof(T)>::type data[N];
std::size_t m_size = 0;
// Create an object in aligned storage
template<typename ...Args> void emplace_back(Args&&... args)
{
if( m_size >= N ) // possible error handling
throw std::bad_alloc{};
// construct value in memory of aligned storage
// using inplace operator new
new(&data[m_size]) T(std::forward<Args>(args)...);
++m_size;
}
// Access an object in aligned storage
const T& operator[](std::size_t pos) const
{
// note: needs std::launder as of C++17
return *reinterpret_cast<const T*>(&data[pos]);
}
// Delete objects from aligned storage
~static_vector()
{
for(std::size_t pos = 0; pos < m_size; ++pos) {
// note: needs std::launder as of C++17
reinterpret_cast<T*>(&data[pos])->~T();
}
}
};
I note the comment in the destructor, "needs std::launder as of C++17",
which I wasn't previously aware of.
It is probably prudent to use it even though it may be important only when T has
5 [Note: If a new object is created in storage occupied by an existing object of
the same type, a pointer
to the original object can be used to refer to the new object unless its
complete object is a const object
or it is a base class subobject; in the latter cases, this function can be used
to obtain a usable pointer
to the new object. ...]
Post by Daniel
Daniel
Pavel
2020-02-25 05:11:05 UTC
Permalink
Post by Pavel
Post by Öö Tiib
Post by Pavel
Post by Daniel
Post by Pavel
Post by Öö Tiib
std::aligned_storage. ;)
There is nothing special I can see about std::aligned_storage with regard to
reinterpret_cast.
Every use I've seen of std::aligned_storage has reinterpret_cast, without
reinterpret_cast, what could you do with it?
It is difficult to say. Somehow the Standard carefully avoids ever guaranteeing
anything about the result of reinterpret_cast<T1*>(tPtr); where tPtr is of type
T* and T is different from T1.
E.g. looking at 7.6.1.9 "Renterpret Cast" in n4849 (and, I believe all previous
3 [Note: The mapping performed by reinterpret_cast might, or might not, produce
a representation different from the original value. — end note]
...
You would think that somewhere in
7 An object pointer can be explicitly converted to an object pointer of a
different type... When a prvalue v of object pointer type is converted to the
object pointer type “pointer to cv T”, the result is static_cast<cv
T*>(static_cast<cv void*>(v)). [Note: Converting a prvalue of type “pointer to
T1” to the type “pointer
to T2” (where T1 and T2 are object types and where the alignment requirements of
T2 are no stricter than
those of T1) and back to its original type yields the original pointer value. —
end note]
the Standard would guarantee something about memory addresses like "the address
of v and the address represented by the result of reinterpret_cast<T2*>(v) are
same" but alas. The guarantees are only for some two-way applications e.g. T* to
intptr_t (see 7.6.1.9-5) and back or T1 to T2 and back.
Of course, everybody uses static_cast and reinterpret_cast for this purpose so
it probably "will just work". Wherever possible I would, however, stick to union
approach which seems to be better defined.
So you could find place where standard says that reinterpret_cast<T*>(v)
is same as static_cast<T*>(static_cast<void*>(v))?
A prvalue of type “pointer to cv1 void” can be converted to a prvalue of type “pointer to cv2 T”, where T is an object type and cv2 is the same cv-qualification as, or greater cv-qualification than, cv1. If the original pointer value represents the address A of a byte in memory and A does not satisfy the alignment requirement of T, then the resulting pointer value is unspecified. Otherwise, if the original pointer value points to an object a, and there is an object b of type T (ignoring cv-qualification) that is pointer-interconvertible (6.9.2) with a, the result is a pointer to b. Otherwise, the pointer value is unchanged by the conversion.
About first member of standard layout struct being pointer-interconvertible
to struct itself I already copy pasted. And aligned_storage
is satisfying the alignment requirements of T so why you deny
that Daniel's example is well-formed?
Because the above text requires that "there is an object b of type T (ignoring
cv-qualification) that is pointer-interconvertible (6.9.2) with a" for result to
be a pointer to b.
How can we deduce that the type std::aligned_storage<T,
some-align-greater-than-or-equal-to-alignof<T> >::type is
pointer-interconvertible with T (even ignoring cv-qualification)?
More precisely I should have asked: \

How can we deduce that *an object of type* std::aligned_storage<T,
some-align-greater-than-or-equal-to-alignof<T> >::type is
pointer-interconvertible with *an object of type* T (even ignoring
cv-qualification)?
Post by Pavel
Post by Öö Tiib
Post by Pavel
Post by Daniel
For instance, the example in
cppreference,
template<class T, std::size_t N>
class static_vector
{
// properly aligned uninitialized storage for N T's
typename std::aligned_storage<sizeof(T), alignof(T)>::type data[N];
std::size_t m_size = 0;
// Create an object in aligned storage
template<typename ...Args> void emplace_back(Args&&... args)
{
if( m_size >= N ) // possible error handling
throw std::bad_alloc{};
// construct value in memory of aligned storage
// using inplace operator new
new(&data[m_size]) T(std::forward<Args>(args)...);
++m_size;
}
// Access an object in aligned storage
const T& operator[](std::size_t pos) const
{
// note: needs std::launder as of C++17
return *reinterpret_cast<const T*>(&data[pos]);
}
// Delete objects from aligned storage
~static_vector()
{
for(std::size_t pos = 0; pos < m_size; ++pos) {
// note: needs std::launder as of C++17
reinterpret_cast<T*>(&data[pos])->~T();
}
}
};
I note the comment in the destructor, "needs std::launder as of C++17",
which I wasn't previously aware of.
It is probably prudent to use it even though it may be important only when T has
5 [Note: If a new object is created in storage occupied by an existing object of
the same type, a pointer
to the original object can be used to refer to the new object unless its
complete object is a const object
or it is a base class subobject; in the latter cases, this function can be used
to obtain a usable pointer
to the new object. ...]
Post by Daniel
Daniel
Cholo Lennon
2020-02-20 01:45:23 UTC
Permalink
Post by Daniel
I've been following the "Union type punning in C++" posts with some interest,
but not exactly sure of the conclusion.
Just for the record: there is a nice talk from CppCon 2019 about "Type
punning in modern C++"



--
Cholo Lennon
Bs.As.
ARG
Pavel
2020-02-25 05:19:09 UTC
Permalink
Post by Daniel
I've been following the "Union type punning in C++" posts with some interest,
but not exactly sure of the conclusion.
Just for the record: there is a nice talk from CppCon 2019 about "Type punning
in modern C++"
http://youtu.be/_qzMpk-22cc
Thanks, it is a nice and somewhat funny talk. Among the others, there is a
chance that the OP's code will become legal in C++ 23 :-).

The guy is in "reinterpret_cast" camp but in the Q&A session he at least
acknowledges the existence of the "union" camp that believes that no use of
reinterpret_cast is legal for type punning. It seems someone from that other
camp wrote an article to disprove reinterpret_cast type punning .. funny I never
heard of it; will try to find and read it when idle i.e. probably also around
2023 when it really does not matter :-).
--
Cholo Lennon
Bs.As.
ARG
-Pavel
Chris M. Thomasson
2020-02-25 06:37:57 UTC
Permalink
Post by Pavel
Post by Daniel
I've been following the "Union type punning in C++" posts with some interest,
but not exactly sure of the conclusion.
Just for the record: there is a nice talk from CppCon 2019 about "Type punning
in modern C++"
http://youtu.be/_qzMpk-22cc
Thanks, it is a nice and somewhat funny talk. Among the others, there is a
chance that the OP's code will become legal in C++ 23 :-).
The guy is in "reinterpret_cast" camp but in the Q&A session he at least
acknowledges the existence of the "union" camp that believes that no use of
reinterpret_cast is legal for type punning. It seems someone from that other
camp wrote an article to disprove reinterpret_cast type punning .. funny I never
heard of it; will try to find and read it when idle i.e. probably also around
2023 when it really does not matter :-).
Fwiw, what I get from the talk is that its better have a ctor and dtor,
or else its undefined regardless? Placement new and explicit dtor call
for properly aligned bytes.
Pavel
2020-02-26 06:07:41 UTC
Permalink
Post by Pavel
Post by Daniel
I've been following the "Union type punning in C++" posts with some interest,
but not exactly sure of the conclusion.
Just for the record: there is a nice talk from CppCon 2019 about "Type punning
in modern C++"
http://youtu.be/_qzMpk-22cc
Thanks, it is a nice and somewhat funny talk. Among the others, there is a
chance that the OP's code will become legal in C++ 23 :-).
The guy is in "reinterpret_cast" camp but in the Q&A session he at least
acknowledges the existence of the "union" camp that believes that no use of
reinterpret_cast is legal for type punning. It seems someone from that other
camp wrote an article to disprove reinterpret_cast type punning .. funny I never
heard of it; will try to find and read it when idle i.e. probably also around
2023 when it really does not matter :-).
Fwiw, what I get from the talk is that its better have a ctor and dtor, or else
its undefined regardless?
My understanding of it was that the object should be created so constructor
should be called as part of that (the constructor implicitly defined by the
implementation should do, too) before using it (to put it in another way, no
amount of pointer casting and arithmetic will help to make the code using the
resulting pointer well defined if an object to which the pointer is trying to
point is not created)
Placement new and explicit dtor call for properly
aligned bytes
Loading...