Sub(-sub)object lifetime and accessibility

472 views
Skip to first unread message

Edward Catmur

unread,
Aug 11, 2016, 3:09:27 PM8/11/16
to ISO C++ Standard - Discussion
Does this program have defined behavior?

#include <new>
struct B { int i; };
extern struct X : B { X() {} } xobj;
struct Y { int j = *std::launder((int*)(&xobj)) += 42; } yobj;
X xobj
;

At the constructor of yobj.j, "referring to any non-static member or base" of xobj is UB, so we cannot write xobj.B::i; but that sub-subobject has begun its lifetime, is not itself a non-static member or base of an object with non-trivial initialization (it is a member of B, which has a trivial constructor), and we know precisely where its storage is located. So can we launder (read/write) access to it?

Similarly, an earlier member may not be allowed to refer to a subobject of a later member, but it may know at which storage location a sub-subobject of that later member lives:

#include <new>
struct B { int i; };
struct X : B { X() {} };
struct Z { int k = *std::launder((int*)(&k + 1)) += 42; X x; } zobj;

I'm not aware of any publicly available compilers with std::launder support available yet, but using [basic.types]/3 I have been able to ascertain that gcc regards this as UB while other compilers (clang, ICC, MSVC) apparently do not (they initialize xobj.i, zobj.x.i each to 42 with nary a complaint).

C++03-compatible test program (both cases):

#include <cstdio>
#include <cstring>
inline int read(void const* p) { int i; memcpy(&i, p, 4); return i; }
inline int write(void* p, int const& i) { memcpy(p, &i, 4); return i; }
struct B { int i; };
struct X : B { X() {} };
extern X xobj;
struct Y { int j; Y() : j(write(&xobj, read(&xobj) + 42)) {} } yobj;
X xobj
;
struct Z { int k; X x; Z() : k(write(&k + 1, read(&k + 1) + 42)) {} } zobj;
int main() { printf("%d %d\n", xobj.i, zobj.x.i); }

Thiago Macieira

unread,
Aug 11, 2016, 5:46:33 PM8/11/16
to std-dis...@isocpp.org
On quinta-feira, 11 de agosto de 2016 12:09:27 PDT Edward Catmur wrote:
> Does this program have defined behavior?
>
> #include <new>
> struct B { int i; };
> extern struct X : B { X() {} } xobj;
> struct Y { int j = *std::launder((int*)(&xobj)) += 42; } yobj;
> X xobj;
>
> At the constructor of yobj.j, "referring to any non-static member or base"
> of xobj is UB, so we cannot write xobj.B::i; but that sub-subobject has
> begun its lifetime, is not itself a non-static member or base of an object
> with non-trivial initialization (it is a member of B, which has a trivial
> constructor), and we know precisely where its storage is located. So can we
> launder (read/write) access to it?

The above Y constructor is equivalent to:

struct *B = &xobj; // accesses base-class B of X, theoretically UB
j = b->i += 42;

But both X and B are trivially constructible and standard layout, like you
said.

> Similarly, an earlier member may not be allowed to refer to a subobject of
> a later member, but it may know at which storage location a sub-subobject
> of that later member lives:
>
> #include <new>
> struct B { int i; };
> struct X : B { X() {} };
> struct Z { int k = *std::launder((int*)(&k + 1)) += 42; X x; } zobj;
>
> I'm not aware of any publicly available compilers with std::launder support
> available yet, but using [basic.types]/3 I have been able to ascertain that
> gcc regards this as UB while other compilers (clang, ICC, MSVC) apparently
> do not (they initialize xobj.i, zobj.x.i each to 42 with nary a complaint).

Well, since you're depending on behaviour not yet implemented, we can't say
much about what compilers are doing or not doing.

> C++03-compatible test program (both cases):
>
> #include <cstdio>
> #include <cstring>
> inline int read(void const* p) { int i; memcpy(&i, p, 4); return i; }
> inline int write(void* p, int const& i) { memcpy(p, &i, 4); return i; }
> struct B { int i; };
> struct X : B { X() {} };
> extern X xobj;
> struct Y { int j; Y() : j(write(&xobj, read(&xobj) + 42)) {} } yobj;
> X xobj;
> struct Z { int k; X x; Z() : k(write(&k + 1, read(&k + 1) + 42)) {} } zobj;
> int main() { printf("%d %d\n", xobj.i, zobj.x.i); }

Under C++03 rules, this is definitely UB. There's no such thing as standard
layout, so all we can say is that Y and Z are non-POD, therefore both your
reads and writes are UB.

--
Thiago Macieira - thiago (AT) macieira.info - thiago (AT) kde.org
Software Architect - Intel Open Source Technology Center

Edward Catmur

unread,
Aug 11, 2016, 6:03:43 PM8/11/16
to std-dis...@isocpp.org
On Thu, Aug 11, 2016 at 10:46 PM, Thiago Macieira <thi...@macieira.org> wrote:
On quinta-feira, 11 de agosto de 2016 12:09:27 PDT Edward Catmur wrote:
> Does this program have defined behavior?
>
> #include <new>
> struct B { int i; };
> extern struct X : B { X() {} } xobj;
> struct Y { int j = *std::launder((int*)(&xobj)) += 42; } yobj;
> X xobj;
>
> At the constructor of yobj.j, "referring to any non-static member or base"
> of xobj is UB, so we cannot write xobj.B::i; but that sub-subobject has
> begun its lifetime, is not itself a non-static member or base of an object
> with non-trivial initialization (it is a member of B, which has a trivial
> constructor), and we know precisely where its storage is located. So can we
> launder (read/write) access to it?

The above Y constructor is equivalent to:

        struct *B = &xobj;              // accesses base-class B of X, theoretically UB
        j = b->i += 42;

But both X and B are trivially constructible and standard layout, like you
said.

Sorry, I'm not clear why that's equivalent; the point of using std::launder was to avoid the upcast. Without the upcast, is it still UB?

> Similarly, an earlier member may not be allowed to refer to a subobject of
> a later member, but it may know at which storage location a sub-subobject
> of that later member lives:
>
> #include <new>
> struct B { int i; };
> struct X : B { X() {} };
> struct Z { int k = *std::launder((int*)(&k + 1)) += 42; X x; } zobj;
>
> I'm not aware of any publicly available compilers with std::launder support
> available yet, but using [basic.types]/3 I have been able to ascertain that
> gcc regards this as UB while other compilers (clang, ICC, MSVC) apparently
> do not (they initialize xobj.i, zobj.x.i each to 42 with nary a complaint).

Well, since you're depending on behaviour not yet implemented, we can't say
much about what compilers are doing or not doing.

Ah, I forgot to mention: the assumption would be that if std::launder is not available then the implementation precedes P0137, so the "point to" rule in [basic.compound]/3 can be relied on.

> C++03-compatible test program (both cases):
>
> #include <cstdio>
> #include <cstring>
> inline int read(void const* p) { int i; memcpy(&i, p, 4); return i; }
> inline int write(void* p, int const& i) { memcpy(p, &i, 4); return i; }
> struct B { int i; };
> struct X : B { X() {} };
> extern X xobj;
> struct Y { int j; Y() : j(write(&xobj, read(&xobj) + 42)) {} } yobj;
> X xobj;
> struct Z { int k; X x; Z() : k(write(&k + 1, read(&k + 1) + 42)) {} } zobj;
> int main() { printf("%d %d\n", xobj.i, zobj.x.i); }

Under C++03 rules, this is definitely UB. There's no such thing as standard
layout, so all we can say is that Y and Z are non-POD, therefore both your
reads and writes are UB.

Thanks, good point.

Richard Smith

unread,
Aug 11, 2016, 6:28:48 PM8/11/16
to std-dis...@isocpp.org
On Thu, Aug 11, 2016 at 12:09 PM, Edward Catmur <e...@catmur.co.uk> wrote:
Does this program have defined behavior?

#include <new>
struct B { int i; };
extern struct X : B { X() {} } xobj;
struct Y { int j = *std::launder((int*)(&xobj)) += 42; } yobj;
X xobj
;

At the constructor of yobj.j, "referring to any non-static member or base" of xobj is UB, so we cannot write xobj.B::i; but that sub-subobject has begun its lifetime, is not itself a non-static member or base of an object with non-trivial initialization (it is a member of B, which has a trivial constructor), and we know precisely where its storage is located. So can we launder (read/write) access to it?

[class.cdtor]/1 seems pretty vague here. It might be talking about class member access and derived-to-base conversions, or it might be talking about performing an access (read / modification) on the corresponding subobject, or something else. But probably the most plausible reading is that it means that any of these actions that could reasonably be considered to be referencing the non-static member or base results in UB (that is, it doesn't matter what concrete syntax you use to get at the non-static member or base).

Similarly, an earlier member may not be allowed to refer to a subobject of a later member, but it may know at which storage location a sub-subobject of that later member lives:

#include <new>
struct B { int i; };
struct X : B { X() {} };
struct Z { int k = *std::launder((int*)(&k + 1)) += 42; X x; } zobj;

I'm not aware of any publicly available compilers with std::launder support available yet, but using [basic.types]/3 I have been able to ascertain that gcc regards this as UB while other compilers (clang, ICC, MSVC) apparently do not (they initialize xobj.i, zobj.x.i each to 42 with nary a complaint).

C++03-compatible test program (both cases):

#include <cstdio>
#include <cstring>
inline int read(void const* p) { int i; memcpy(&i, p, 4); return i; }
inline int write(void* p, int const& i) { memcpy(p, &i, 4); return i; }
struct B { int i; };
struct X : B { X() {} };
extern X xobj;
struct Y { int j; Y() : j(write(&xobj, read(&xobj) + 42)) {} } yobj;
X xobj
;
struct Z { int k; X x; Z() : k(write(&k + 1, read(&k + 1) + 42)) {} } zobj;
int main() { printf("%d %d\n", xobj.i, zobj.x.i); }

--

---
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Discussion" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-discussion+unsubscribe@isocpp.org.
To post to this group, send email to std-dis...@isocpp.org.
Visit this group at https://groups.google.com/a/isocpp.org/group/std-discussion/.

Thiago Macieira

unread,
Aug 11, 2016, 9:39:37 PM8/11/16
to std-dis...@isocpp.org
On quinta-feira, 11 de agosto de 2016 23:03:40 PDT 'Edward Catmur' via ISO C++
Standard - Discussion wrote:
> On Thu, Aug 11, 2016 at 10:46 PM, Thiago Macieira <thi...@macieira.org>
> wrote:
> > The above Y constructor is equivalent to:
> > struct *B = &xobj; // accesses base-class B of X,
> >
> > theoretically UB
> >
> > j = b->i += 42;
> >
> > But both X and B are trivially constructible and standard layout, like you
> > said.

I need to correct myself: B is trivially constructible, but X isn't.

> Sorry, I'm not clear why that's equivalent; the point of using std::launder
> was to avoid the upcast. Without the upcast, is it still UB?

Doesn't matter what you laundered if the compiler assumes you didn't write.

> > Well, since you're depending on behaviour not yet implemented, we can't
> > say
> > much about what compilers are doing or not doing.
>
> Ah, I forgot to mention: the assumption would be that if std::launder is
> not available then the implementation precedes P0137, so the "point to"
> rule in [basic.compound]/3 can be relied on.

I don't see the point. This goes back to the discussion on the other thread
about what happens if you write to the storage of an object before its
lifetime begins. Until that is resolved, this discussion is moot.

Edward Catmur

unread,
Aug 12, 2016, 9:37:41 AM8/12/16
to std-dis...@isocpp.org
On Thu, Aug 11, 2016 at 11:28 PM, Richard Smith <ric...@metafoo.co.uk> wrote:
On Thu, Aug 11, 2016 at 12:09 PM, Edward Catmur <e...@catmur.co.uk> wrote:
Does this program have defined behavior?

#include <new>
struct B { int i; };
extern struct X : B { X() {} } xobj;
struct Y { int j = *std::launder((int*)(&xobj)) += 42; } yobj;
X xobj
;

At the constructor of yobj.j, "referring to any non-static member or base" of xobj is UB, so we cannot write xobj.B::i; but that sub-subobject has begun its lifetime, is not itself a non-static member or base of an object with non-trivial initialization (it is a member of B, which has a trivial constructor), and we know precisely where its storage is located. So can we launder (read/write) access to it?

[class.cdtor]/1 seems pretty vague here. It might be talking about class member access and derived-to-base conversions, or it might be talking about performing an access (read / modification) on the corresponding subobject, or something else. But probably the most plausible reading is that it means that any of these actions that could reasonably be considered to be referencing the non-static member or base results in UB (that is, it doesn't matter what concrete syntax you use to get at the non-static member or base).

That's plausible, but problematic; it appears to imply that the entire storage of the complete object gets tainted by the prospect of the complete object being constructed there in the future.

Or is this [basic.life]/7 referring to [class.cdtor]/1, and the storage is only tainted while the complete object is under construction/destruction?

Edward Catmur

unread,
Aug 12, 2016, 9:47:33 AM8/12/16
to std-dis...@isocpp.org
On Fri, Aug 12, 2016 at 2:39 AM, Thiago Macieira <thi...@macieira.org> wrote:
On quinta-feira, 11 de agosto de 2016 23:03:40 PDT 'Edward Catmur' via ISO C++
Standard - Discussion wrote:
> On Thu, Aug 11, 2016 at 10:46 PM, Thiago Macieira <thi...@macieira.org>
> wrote:
> > The above Y constructor is equivalent to:
> >         struct *B = &xobj;              // accesses base-class B of X,
> >
> > theoretically UB
> >
> >         j = b->i += 42;
> >
> > But both X and B are trivially constructible and standard layout, like you
> > said.

I need to correct myself: B is trivially constructible, but X isn't.

> Sorry, I'm not clear why that's equivalent; the point of using std::launder
> was to avoid the upcast. Without the upcast, is it still UB?

Doesn't matter what you laundered if the compiler assumes you didn't write.

Why would it be entitled to make that assumption? std::launder is described as an optimization barrier.

> > Well, since you're depending on behaviour not yet implemented, we can't
> > say
> > much about what compilers are doing or not doing.
>
> Ah, I forgot to mention: the assumption would be that if std::launder is
> not available then the implementation precedes P0137, so the "point to"
> rule in [basic.compound]/3 can be relied on.

I don't see the point. This goes back to the discussion on the other thread
about what happens if you write to the storage of an object before its
lifetime begins. Until that is resolved, this discussion is moot.

Writing to the storage of a composite object before its lifetime begins is not a problem, as long as that composite object's constructor has been entered ([class.cdtor]/1). Here I am exploring a situation where a composite object's constructor has not yet been entered, but its initialization has begun and the lifetime of a nested primitive subobject has begun. The motivation is to tease apart the concepts of storage duration, lifetime, initialization and construction.

Thiago Macieira

unread,
Aug 12, 2016, 12:17:58 PM8/12/16
to std-dis...@isocpp.org
On sexta-feira, 12 de agosto de 2016 14:47:31 PDT 'Edward Catmur' via ISO C++
Standard - Discussion wrote:
> > Doesn't matter what you laundered if the compiler assumes you didn't
> > write.
>
> Why would it be entitled to make that assumption? std::launder is described
> as an optimization barrier.

Yes. It may have to discard all known values to all variables, but that
doesn't mean anything about memory it hasn't begun to use. For example,
imagine that you used std::launder to write to a memory location of an unborn
stack variable:


int x = 0;
*std::launder((&x)[-1]) = 1;

int y;
assert(y == 1);

Is the compiler required to make that assertion pass? I'd say not, not even if
I moved the y declaration above the launder.

This is where the discussion of storage area values vs constructors comes in.
If the compiler is allowed to assume that the storage area doesn't contain
anything useful except for static-lifetime objects, then it can assume you
didn't launder something.

> > I don't see the point. This goes back to the discussion on the other
> > thread
> > about what happens if you write to the storage of an object before its
> > lifetime begins. Until that is resolved, this discussion is moot.
>
> Writing to the storage of a composite object before its lifetime begins is
> not a problem, as long as that composite object's constructor has been
> entered ([class.cdtor]/1). Here I am exploring a situation where a
> composite object's constructor has not yet been entered, but its
> initialization has begun and the lifetime of a nested primitive subobject
> has begun. The motivation is to tease apart the concepts of storage
> duration, lifetime, initialization and construction.

Indeed. That is what I'd like to see concluded. std::launder is a distracting
discussion.

Edward Catmur

unread,
Aug 12, 2016, 1:21:45 PM8/12/16
to std-dis...@isocpp.org
On Fri, Aug 12, 2016 at 5:17 PM, Thiago Macieira <thi...@macieira.org> wrote:
On sexta-feira, 12 de agosto de 2016 14:47:31 PDT 'Edward Catmur' via ISO C++
Standard - Discussion wrote:
> > Doesn't matter what you laundered if the compiler assumes you didn't
> > write.
>
> Why would it be entitled to make that assumption? std::launder is described
> as an optimization barrier.

Yes. It may have to discard all known values to all variables, but that
doesn't mean anything about memory it hasn't begun to use. For example,
imagine that you used std::launder to write to a memory location of an unborn
stack variable:


        int x = 0;
        *std::launder((&x)[-1]) = 1;

        int y;
        assert(y == 1);

Is the compiler required to make that assertion pass? I'd say not, not even if
I moved the y declaration above the launder.

Correct, but that's because x and y do not share a storage area, so y is not reachable from x; [ptr.launder]/1 and /3.

This is where the discussion of storage area values vs constructors comes in.
If the compiler is allowed to assume that the storage area doesn't contain
anything useful except for static-lifetime objects, then it can assume you
didn't launder something.

But that's just it; I am writing via a pointer that can be used to reach a static-lifetime object located in the storage area, so the compiler has to assume that my use of launder could modify any of the objects whose lifetime has begun at that point.

> > I don't see the point. This goes back to the discussion on the other
> > thread
> > about what happens if you write to the storage of an object before its
> > lifetime begins. Until that is resolved, this discussion is moot.
>
> Writing to the storage of a composite object before its lifetime begins is
> not a problem, as long as that composite object's constructor has been
> entered ([class.cdtor]/1). Here I am exploring a situation where a
> composite object's constructor has not yet been entered, but its
> initialization has begun and the lifetime of a nested primitive subobject
> has begun. The motivation is to tease apart the concepts of storage
> duration, lifetime, initialization and construction.

Indeed. That is what I'd like to see concluded. std::launder is a distracting
discussion.

--
Thiago Macieira - thiago (AT) macieira.info - thiago (AT) kde.org
   Software Architect - Intel Open Source Technology Center

--

---
You received this message because you are subscribed to a topic in the Google Groups "ISO C++ Standard - Discussion" group.
To unsubscribe from this topic, visit https://groups.google.com/a/isocpp.org/d/topic/std-discussion/XYvVlTc3-to/unsubscribe.
To unsubscribe from this group and all its topics, send an email to std-discussion+unsubscribe@isocpp.org.

Thiago Macieira

unread,
Aug 12, 2016, 1:53:32 PM8/12/16
to std-dis...@isocpp.org
On sexta-feira, 12 de agosto de 2016 18:21:43 PDT 'Edward Catmur' via ISO C++
Standard - Discussion wrote:
> > If the compiler is allowed to assume that the storage area doesn't contain
> > anything useful except for static-lifetime objects, then it can assume you
> > didn't launder something.
>
> But that's just it; I am writing via a pointer that can be used to reach a
> static-lifetime object located in the storage area, so the compiler has to
> assume that my use of launder could modify any of the objects whose
> lifetime has begun at that point.

The problem we're facing here is that a complex objects have complex rules for
the beginning of lifetime. When does the lifetime of a sub-object begin, when
its constructor starts or when the whole object's constructor does?

Also, the only acceptable value for the storage of static-lifetime objects is
zero: they are zero-initialised prior to dynamic construction. Trivially-
constructible objects can be value-initialised after the zeroing pass, but
that's not the case here.

Edward Catmur

unread,
Aug 12, 2016, 2:39:16 PM8/12/16
to std-dis...@isocpp.org
On Fri, Aug 12, 2016 at 6:53 PM, Thiago Macieira <thi...@macieira.org> wrote:
On sexta-feira, 12 de agosto de 2016 18:21:43 PDT 'Edward Catmur' via ISO C++
Standard - Discussion wrote:
> > If the compiler is allowed to assume that the storage area doesn't contain
> > anything useful except for static-lifetime objects, then it can assume you
> > didn't launder something.
>
> But that's just it; I am writing via a pointer that can be used to reach a
> static-lifetime object located in the storage area, so the compiler has to
> assume that my use of launder could modify any of the objects whose
> lifetime has begun at that point.

The problem we're facing here is that a complex objects have complex rules for
the beginning of lifetime. When does the lifetime of a sub-object begin, when
its constructor starts or when the whole object's constructor does?

I don't see any reason to expect that [basic.life]/1 would not apply here; that clause specifically calls out union member subobjects so by expressio unius the lifetime of subobjects that are not union members is determined solely by their own storage and initialization, not that of their complete object.

Also, the only acceptable value for the storage of static-lifetime objects is
zero: they are zero-initialised prior to dynamic construction. Trivially-
constructible objects can be value-initialised after the zeroing pass, but
that's not the case here.

Ah, that's fair enough; it would be desirable for a compiler to depend on vacuously initialized subobjects of a static duration complete object retaining their zero values through initialization and construction. I continue to believe that this will require language change (though that will formalize only what compilers are doing already).

Kazutoshi Satoda

unread,
Aug 16, 2016, 4:43:40 AM8/16/16
to std-dis...@isocpp.org
On 2016/08/12 4:09 +0900, Edward Catmur wrote:
> Does this program have defined behavior?
>
> #include <new>
> struct B { int i; };
> extern struct X : B { X() {} } xobj;
> struct Y { int j = *std::launder((int*)(&xobj)) += 42; } yobj;
> X xobj;

(I think this point is not so important for the whole discussion, though...)

I can't see any effect of std::launder() here to say the defined-ness
of the program.

AFAIK, std::launder() has an effect only when
- a storage for an object A is reused for another object B,
- and
- the type of A is not similar to the type of B,
- or the type of the objects is (or has) a const subobject.
Then the above "*std::launder((int*)(&xobj))" is equivalent to just
"*(int*)(&xobj)" or "*reinterpret_cast<int*>(&xobj)".

Am I missing something?

--
k_satoda

Edward Catmur

unread,
Aug 16, 2016, 5:15:17 AM8/16/16
to std-dis...@isocpp.org

Yes, I think so. That reinterpret_cast is illegal because &xobj cast to int* does not point to an object of type int. It would be legal in C++14 and previous versions, where a pointer automatically points to an object whose address is the same as the address held by the pointer.

std::launder works not only when storage is reused, but whenever two objects occupy the same storage location and are mutually reachable; there is no requirement that they have disjoint lifetimes.

Kazutoshi Satoda

unread,
Aug 16, 2016, 5:44:48 AM8/16/16
to std-dis...@isocpp.org
On 2016/08/16 18:15 +0900, 'Edward Catmur' via ISO C++ Standard - Discussion wrote:
> On 16 Aug 2016 9:43 a.m., "Kazutoshi Satoda" <k_sa...@f2.dion.ne.jp> wrote:
>> On 2016/08/12 4:09 +0900, Edward Catmur wrote:
>> > Does this program have defined behavior?
>> >
>> > #include <new>
>> > struct B { int i; };
>> > extern struct X : B { X() {} } xobj;
>> > struct Y { int j = *std::launder((int*)(&xobj)) += 42; } yobj;
>> > X xobj;
...
>> I can't see any effect of std::launder() here to say the defined-ness
>> of the program.
>>
>> AFAIK, std::launder() has an effect only when
>> - a storage for an object A is reused for another object B,
>> - and
>> - the type of A is not similar to the type of B,
>> - or the type of the objects is (or has) a const subobject.
>> Then the above "*std::launder((int*)(&xobj))" is equivalent to just
>> "*(int*)(&xobj)" or "*reinterpret_cast<int*>(&xobj)".
>>
>> Am I missing something?
>
> Yes, I think so. That reinterpret_cast is illegal because &xobj cast to
> int* does not point to an object of type int. It would be legal in C++14
> and previous versions, where a pointer automatically points to an object
> whose address is the same as the address held by the pointer.

That reinterpret_cast seems to be allowed by the new
"pointer-interconvertible" rule. In your code, xobj and xobj.i are
pointer-interconvertible, according to the latest draft N4606, 3.9.2
[basic.compound] p4:
> Two objects a and b are pointer-interconvertible if:
> - they are the same object, or
> - one is a standard-layout union object and the other is a non-static
> data member of that object (9.3), or
> - 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 (9.2), or
> - 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 (5.2.10). [ Note: An array object and
> its first element are not pointer-interconvertible, even though they
> have the same address. -end note ]

This rule was added in p0137r1 as changes after R0.
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0137r1.html
> - reallow casts between pointer types to work in a small handful of C-compatible cases (between a struct and its first member etc)

But your interpretation seems also possible. It will be clear if the
wording was something like "... possible to obtain a pointer which point
to one from a pointer which point to the other via a reinterpret_cast."
Do you agree that this is intended?

> std::launder works not only when storage is reused, but whenever two
> objects occupy the same storage location and are mutually reachable; there
> is no requirement that they have disjoint lifetimes.

Assuming the pointer-interconvertible rule covers the cases like your
code, "An array object and its first element" shown in the note may be
the only such case.

--
k_satoda

Richard Smith

unread,
Aug 16, 2016, 2:08:33 PM8/16/16
to std-dis...@isocpp.org
See [basic.compound]/3.1: a "pointer to [an object]" is the base term, "point to" is defined in terms of that. So yes, your formulation is a more verbose way of saying the same thing.

> std::launder works not only when storage is reused, but whenever two
> objects occupy the same storage location and are mutually reachable; there
> is no requirement that they have disjoint lifetimes.

Assuming the pointer-interconvertible rule covers the cases like your
code, "An array object and its first element" shown in the note may be
the only such case.

It's actually trivial to come up with more cases. For instance, if X were not standard-layout, and its base class B still happened to be at the start of the X object.

Kazutoshi Satoda

unread,
Aug 17, 2016, 2:44:51 PM8/17/16
to std-dis...@isocpp.org
On 2016/08/17 3:08 +0900, Richard Smith wrote:
> On Tue, Aug 16, 2016 at 2:44 AM, Kazutoshi Satoda <k_sa...@f2.dion.ne.jp> wrote:
>> On 2016/08/16 18:15 +0900, 'Edward Catmur' via ISO C++ Standard - Discussion wrote:
>>> On 16 Aug 2016 9:43 a.m., "Kazutoshi Satoda" <k_sa...@f2.dion.ne.jp> wrote:
...
>>>> Am I missing something?
>>>
>>> Yes, I think so. That reinterpret_cast is illegal because &xobj cast to
>>> int* does not point to an object of type int. It would be legal in C++14
>>> and previous versions, where a pointer automatically points to an object
>>> whose address is the same as the address held by the pointer.
...
>> But your interpretation seems also possible. It will be clear if the
>> wording was something like "... possible to obtain a pointer which point
>> to one from a pointer which point to the other via a reinterpret_cast."
>> Do you agree that this is intended?
>
> See [basic.compound]/3.1: a "pointer to [an object]" is the base term,
> "point to" is defined in terms of that. So yes, your formulation is a more
> verbose way of saying the same thing.

Uh, I confused "pointer to an object" and "pointer to a type". You are
right. Thank you.

>>> std::launder works not only when storage is reused, but whenever two
>>> objects occupy the same storage location and are mutually reachable; there
>>> is no requirement that they have disjoint lifetimes.
>>
>> Assuming the pointer-interconvertible rule covers the cases like your
>> code, "An array object and its first element" shown in the note may be
>> the only such case.
>
> It's actually trivial to come up with more cases. For instance, if X were
> not standard-layout, and its base class B still happened to be at the start
> of the X object.

Thinking about such a case by the following code:
(http://melpon.org/wandbox/permlink/hmszsqs2lKmIOxSQ)

#include <type_traits>
#include <cassert>
struct Base { int i; virtual ~Base() {} };
struct Derived : Base {};
static_assert(not std::is_standard_layout_v<Derived>);
int main()
{
Derived d{};
assert(d.i == 0);
Base* pb = reinterpret_cast<Base*>(&d);
assert(pb == static_cast<void*>(&d));
// Given pb represents the address of the first byte of d,
// what rule invalidates this access?
return pb->i;
}

I can't locate such a rule that invalidates the last pb-i access.
Could you or someone else?

My interpretation:
The result of reinterpret_cast<Base*>(&d), which is the value of pb, is
defined as static_cast<Base*>(static_cast<void*>(&d)) (in 5.2.10 p7),
and the both static_cast are defined as "the pointer value is unchanged
by the conversion" (4.11 p2, 5.2.9 p13) then the result points to d as
&d does. When the assertion "pb == ..." fails, the access is invalid
because of the aliasing rule (3.10 p8). But if the assertion succeeds
(which means the Base subobject happened to be at the start of the
Derived object) the access seems to be allowed by the aliasing rule as
an access to Derived object through a glvalue of Base.

As "pb points d" is very strange outcome, I think I'm missing some
existing rule. Sorry if it is an obvious one.

--
k_satoda

Richard Smith

unread,
Aug 17, 2016, 5:06:51 PM8/17/16
to std-dis...@isocpp.org
The Base and Derived objects are not pointer-interconvertible, because Derived is not a standard-layout class type. Therefore pb is a pointer of type Base* that points to the d object, not to its Base subobject.
 
    assert(pb == static_cast<void*>(&d));
    // Given pb represents the address of the first byte of d,
    // what rule invalidates this access?
    return pb->i;

Here, pb does not point to an object of type B, and a class member access is *supposed to* result in undefined behavior in this case. But [expr.ref]/4.2 doesn't appear to actually say that :-( That seems like a defect to me.

  }

I can't locate such a rule that invalidates the last pb-i access.
Could you or someone else?

My interpretation:
The result of reinterpret_cast<Base*>(&d), which is the value of pb, is
defined as static_cast<Base*>(static_cast<void*>(&d)) (in 5.2.10 p7),
and the both static_cast are defined as "the pointer value is unchanged
by the conversion" (4.11 p2, 5.2.9 p13) then the result points to d as
&d does. When the assertion "pb == ..." fails, the access is invalid
because of the aliasing rule (3.10 p8). But if the assertion succeeds
(which means the Base subobject happened to be at the start of the
Derived object) the access seems to be allowed by the aliasing rule as
an access to Derived object through a glvalue of Base.

The access (the load performed by the lvalue-to-rvalue conversion) is on a glvalue of type int -- [basic.lval]/8 means "access" in the sense of [defns.access], not in the sense of class member access (which is paradoxically not an access).

As "pb points d" is very strange outcome, I think I'm missing some
existing rule. Sorry if it is an obvious one.

--
k_satoda

--

---

You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Discussion" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-discussion+unsubscribe@isocpp.org.

Kazutoshi Satoda

unread,
Aug 28, 2016, 6:07:25 AM8/28/16
to std-dis...@isocpp.org
On 2016/08/18 6:06 +0900, Richard Smith wrote:
> On Wed, Aug 17, 2016 at 11:44 AM, Kazutoshi Satoda <k_sa...@f2.dion.ne.jp> wrote:
>> On 2016/08/17 3:08 +0900, Richard Smith wrote:
>>> On Tue, Aug 16, 2016 at 2:44 AM, Kazutoshi Satoda <k_sa...@f2.dion.ne.jp> wrote:
>>>> On 2016/08/16 18:15 +0900, 'Edward Catmur' via ISO C++ Standard - Discussion wrote:
...
>>>>> std::launder works not only when storage is reused, but whenever two
>>>>> objects occupy the same storage location and are mutually reachable; there
>>>>> is no requirement that they have disjoint lifetimes.
>>>>
>>>> Assuming the pointer-interconvertible rule covers the cases like your
>>>> code, "An array object and its first element" shown in the note may be
>>>> the only such case.
>>>
>>> It's actually trivial to come up with more cases. For instance, if X were
>>> not standard-layout, and its base class B still happened to be at the start
>>> of the X object.
>>
>> Thinking about such a case by the following code:
>> (http://melpon.org/wandbox/permlink/hmszsqs2lKmIOxSQ)
>>
>> #include <type_traits>
>> #include <cassert>
>> struct Base { int i; virtual ~Base() {} };
>> struct Derived : Base {};
>> static_assert(not std::is_standard_layout_v<Derived>);
>> int main()
>> {
>> Derived d{};
>> assert(d.i == 0);
>> Base* pb = reinterpret_cast<Base*>(&d);
>
> The Base and Derived objects are not pointer-interconvertible, because
> Derived is not a standard-layout class type. Therefore pb is a pointer of
> type Base* that points to the d object, not to its Base subobject.

Agreed.

I suspect there are some problematic wordings which are not tolerant to
the possibility of "Base* can point a Derived (or other unrelated type)
object". Moved to std-proposals:
https://groups.google.com/a/isocpp.org/d/topic/std-proposals/JNOt5yeyQAU/discussion

--
k_satoda
Reply all
Reply to author
Forward
0 new messages