I hope not but it depends on your definition of fruitless.
> but here are the main provisions of the
> C++17 standard which define the behaviour of this code. I would have
> hoped that it would have been apparent from my earlier posts, but here
> it is in extenso:
>
> §4.5/3 (a complete object may be constructed in storage comprising
> an array of unsigned char or of std::byte)
>
> §6.8/10 (an object may be accessed through a glvalue of, and
> therefore aliased by a pointer to, its dynamic type or amongst other
> things a char, unsigned char or std::byte type)
this does not seem relevant as reinterpret_cast does not try to access any
object by glvalue it simply converts that glvalue itself (the pointer); and the
subsequent attempt to access the object is done by a pointer already converted
to the "object dynamic type", so you have nothing to prove here.
(*) Also, just for the record, &s in the above code snippet is *not* a pointer
to an object of type std::byte which you seem to imply -- please also see below.
>
> §8.2.9/1 (static_cast<T>(v) converts the expression v to type T,
> where it is one of the cases covered by §8.2.9)
>
> §8.2.9/13 (the expression static_cast<T*>(static_cast<void*>(p1))
> returns the pointer value unchanged (except as to type) where, as
> in our case, the type of p1 and T* are not pointer-interconvertible,
> provided that the alignment requirement of type T is met).
>
> §8.2.10/7 (an object pointer can be converted by reinterpret_cast to
> an object pointer of a different type: reinterpret_cast<cv T*>(v) is
> equivalent to static_cast<cv T*>(static_cast<cv void*>(v)))
>
> §8.3.6/1 ((alignof T) yields the alignment requirement of type T)
>
> §10.6.2/3 (alignas(T) is the same as (alignas (alignof T)), and
> results in its operand meeting the alignment requirement of T)
>
> §
21.6.2.3/1 (the return value of the placement new operator for single
> objects is its placement pointer value unchanged, and that operator
> performs no other action than return it)
>
> §21.6.4 (std::launder permits access to an object that is within its
> lifetime and whose storage is located at the address given by its
> operand - say, in an array of unsigned char or std::byte, as the
> corollary of §4.5/3).
>
> You will I suspect be inclined to dismiss all this.
No. I would rather use it to try to pinpoint the root cause of the disagreement.
I think (please correct me if I am wrong) that you believe that given this code:
(**)
struct T2 { double d = 2.0; }; // line 1
struct alignas(T2) T1 { char ca[8]; }; // line 2
static_assert(sizeof(T1) == sizeof(T2)); // line 3
static_assert(alignof(T1) == alignof(T2)); // line 4
T1* const p1 = new T1; // line 5
p1->~T1(); // line 6
new (p1) T2; // line 7
T2* const p2 = reinterpret_cast<T2*>(p1); // line 8
cout << p2->d * 2.0 << endl; // line 9
and assuming it compiles (i.e. size and alignment are ok; also let's pretend
that launder and aliasing do not exist; we may have separate disagreement about
these but IMHO they are irrelevant here), the Standard guarantees that p2 points
to a [complete] object o2 of class T2 and hence the code "will print 777".
To the contrary, I think that the closest thing to the above that the Standard
guarantees is that the "pointer values" of p1 and p2 are same. This is not
enough and here is the proof: (text below are direct citations, from n4849):
(a) [definition of memory address]:
6.7
Memory and objects
6.7.1
Memory model
1 The fundamental storage unit in the C ++ memory model is the byte. ... The
memory available to a C ++ program consists of one or more sequences of
contiguous bytes. Every byte has a unique address.
...
6.8.2 Compound types:
(b) [definition of pointer value via memory address (a value of a pointer
type])] (continuation of paragraph 3 after (3.4))
3
...
A value of a pointer type that is a pointer to or past the end of an object
represents the address of the first byte in memory (6.7.1) occupied by the
object ...
(c) [definition of when reinterpret_cast can be used to convert pointers to
objects] end-of
6.8.2-4:
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 (7.6.1.9).
(d) [an example of objects' having same address (hence, by definition, pointers
to these objects have same pointer value) but being not
pointer-interconvertible], ibid
[Note: An array object and its first element are not pointer-interconvertible,
even though they have the same address. — end note]
BTW see (*) above about the &s from ccpreference example under discussion not
being a pointer to std::byte. Only mentioning this here as you seem to imply
(please correct me that I am wrong) that accessing an object via the result of
reinterpret_cast from a pointer to std::byte, char etc. when the object is not
pointer-interconvertible with any of these types is somehow "more legal". (I
happen to think these accesses have undefined behaviors; these types are only
discussed specially because of being minimal units of memory and having minimal
alignment).
You may object that in practice the pointers representing same addresses are
identical and hence differentiating between the pointers of type "pointer to T"
with same "pointer values" and those pointing to same object does not make sense
but consider these 2 counterarguments first:
(e) compiler can produce better (e.g. faster or smaller) code if it does not
need to guarantee any defined behavior to the code that may access objects
through a pointer obtained via reinterpret_cast.
(f) consider CPU architecture (simplified) with:
- 64-bit pointers and 63-bit memory addresses;
- each instruction (OP) consisting of OP code, a register operand and a memory
operand (MEND);
- CPU operating on the objects of types naturally mapping to C++ integral,
pointer and floating point types where U64 maps to uint64_6, MEND to pointer and
DBL to double
- OP codes specifying operation and operand size but not type
- two ALUs, ALU0 handling floating point objects and ALU1 all others
- most significant bit (MSB) of a MEND specifying the ALU where CPU scheduler
should direct the OP for processing; and the 63 LSBs representing the address in
memory.
Now try to mentally run the code snippet (**) on a computer system with the CPU
described above:
(g) assume for the sake of example that the operator new in line 5 returns
0x0000000000000010; (wisely basing the decision to not set MSB on knowing that
the first member of T is an array of char (i.e. not a double or array thereof)
and hence it will likely be sent to ALU0
(h) what should reinterpret_cast in line 8 return? It is clearly more efficient
to do nothing than do something so unless the Standard makes it to return a
pointer that is pointer-interconvertible to a pointer to double (which the
Standard does not), it will return 0x0000000000000010.
NOTE: In optimized code this reinterpret_cast will be a no-op (which is how
reinterpret_cast has always been and how it has been intended to be -- a rare
unity); compiler will simply take note that to get hold of `p2' from this point
in the source code and on it needs to use same register that it already
allocated for `p1' and to which it has already loaded the `p1' (Save a register
-- save the World (C) :-) ).
(i) what effect should line 9 produce? I think it may even print something..
maybe.. unless it crashes before.. but unlikely 4.. but still possible.
"Classic" UB as it is often explained in C++ primers (if explains at all).
(j) You may argue that this is such a weird architecture as in (f) above,
although possible (why not?), is uncommon. I do not think it is weird, but ok
for the sake of the example; but as for the commonality I will refer you to a
very broadly used i8086 in "real CPU mode" where every address in its 1-Mb
address space has a variable number of representations (usually >1 and high)
depending on its value and, furthermore, all representations of an address can
be used for some purposes but only certain -- for other purposes.
> If so, can I
> invite you to consider three additional points. First, as I have
> previously pointed out, the authors of
cppreference.com think the code
> is correct and I suspect they know more about it than you do.
> Secondly, as I have also pointed out, Stroustrup's TC++PL contains a
> similar example (using std::uninitialized_copy()) and I suspect he
> knows more about it than you do.
>
> Thirdly you might then respond "that's an argument ad verecundiam",
Yes. People make mistakes. A productive person makes more mistakes than an
average person. Of course s/he also produces many more useful
goods/services/ideas than an average person.
> and
> indeed it is, although I have also given you the argument on the facts.
> Let me alternatively appeal to common sense and give an example of a
> case where you cannot feasibly directly use the return value of
> placement new (as opposed to the buffer value passed to it) in the way
> you have previously suggested. This is the very common idiom of using
> an array of unsigned char or std::byte to construct an uninitialized
> buffer for objects of class type T, let's say to make a fixed-size
> circular buffer of up to 100 T elements. For circular buffers you
> would commonly construct an element in the buffer using placement new,
> and then, when you subsequently copy/move the element from the buffer,
> destroy the buffer element by calling its destructor explicitly. You
> could not however store the pointer value returned by the application of
> placement new without keeping a separate array of T* pointers to shadow
> each of the possible 100 or less T elements in the array of unsigned
> char at any one time. That would be a ridiculous waste of memory and
> even if you doubt the competence of
cppreference.com and/or Stroustrup,
> you must surely think that the authors of the C++ standard would have
> taken the effort to cover this, particularly given that the standard
> explicitly permits the construction of objects in arrays of unsigned
> char or std::byte.
>
Let me try address this argument. Its essence is (please correct me if I am
wrong), that it impossible to access the results of placement new without
stashing the returned pointer or using reinterpret_cast somewhere and by that
over-complicating the program. I happen to believe that it is, on the contrary,
possible wherever the desired behavior is actually achievable in C++. I would
not take on proving such a broad statement "in general" but I am willing to
solve a specific puzzle (i.e. refactor given code with reasonable *expected*
behavior to ditch the use of reinterpret_cast on not pointer-interconvertible
objects and (hopefully, maybe) even improve its readability) as long it is short
and to the point and do it without resorting to stashing the result of the
placement new (other than maybe for illustration).
As an example, below is a copy of the code from the thread "Union type punning
in C++ redux" where the OP asked about a variant class code they implemented
with reinterpret_cast as follows:
// original OP's code begins ----------------
#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;
}
};
// original OP's code ends ----------------
// the replacement for code of V begins ----------------
// (the rest of the original code can be used "as-is";
// you will notice I changed the type of parameter
// `n' of V2() but that is only because I believed
// it was the typo on behalf of the OP so that change seems
// irrelevant to the topic
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; }
};
// replacement for code of V ends ----------------