15
votes

Inspired from this question.

struct E {};
E e;
E f(e);  // Accesses e?

To access is to

read or modify the value of an object

The empty class has an implicitly defined copy constructor

The implicitly-defined copy/move constructor for a non-union class X performs a memberwise copy/move of its bases and members. [...] The order of initialization is the same as the order of initialization of bases and members in a user-defined constructor. Let x be either the parameter of the constructor or, for the move constructor, an xvalue referring to the parameter. Each base or non-static data member is copied/moved in the manner appropriate to its type:

  • [...] the base or member is direct-initialized with the corresponding base or member of x.
2
@YSC the evaluation of *pe by itself is UB right away, so no.Quentin
given that sizeof(E) is 1; I would expect that 1 byte to be copied. Even if it is completely unused (which would count as a read)UKMonkey
@UKMonkey But the copy constructor looks like its defined to do nothing, the single byte is probably considered paddingPasser By
@Quentin No, it's not. We've (the language-lawyers) had this discussion. However, it is undefined to bind a reference to a null lvalue, certainly.Columbo
@Columbo did you? Is there a link I could follow?YSC

2 Answers

3
votes

I think that the part of the standard that describes the most precisely what performs an access is [basic.life]. In this paragraph it is explained what can be done with a reference that refers to, or a pointer that point to, an object which is out of its lifetime period. Everything that is authorized to do with such entities does not perform an access to the object value since such value does not exist (otherwise the standard would be inconsistent).

So we can take a more drastic example, if this is not undefined behavior, so there are no access to e in your example code (accordingly to the reasonning above):

struct E{
     E()=default;
     E(const E&){}
     };
E e;
e.~E();
E f(e);

Here e is an object whose lifetime has ended but whose storage is still allocated. What can be done with such a lvalue is described in [basic.life]/6

Similarly, before the lifetime of an object has started but after the storage which the object will occupy has been allocated or, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, any glvalue that refers to the original object may be used but only in limited ways. For an object under construction or destruction, see [class.cdtor]. Otherwise, such a glvalue refers to allocated storage ([basic.stc.dynamic.deallocation]), and using the properties of the glvalue that do not depend on its value is well-defined. The program has undefined behavior if:

  • an lvalue-to-rvalue conversion ([conv.lval]) is applied to such a glvalue,

  • the glvalue is used to access a non-static data member or call a non-static member function of the object, or

  • the glvalue is implicitly converted ([conv.ptr]) to a reference to a base class type, or

  • the glvalue is used as the operand of a static_cast ([expr.static.cast]) except when the conversion is ultimately to cv char& or cv unsigned char&, or

  • the glvalue is used as the operand of a dynamic_cast ([expr.dynamic.cast]) or as the operand of typeid.

None of the cited point above does happen inside E copy constructor so the example code in this answer is well defined, which implies that there have been no access to the value of the destroyed object. So there is no access to e in your example code.

2
votes

I think it does not access the object, though a valid object is required to be present.

E f(e);

This calls E's implicitly defined constructor E::E(const E&). Obviously the body of this constructor is empty (because there is nothing to do). So if anything happens, it must happen during argument passing, i.e. during the initialization of const E& from e.

It is self-evident that this initialization does not modify e. Now, to read the value of e, a lvalue-to-rvalue conversion must take place. However, the standard actually says that this conversion does not take place during direct reference binding1. That is to say, no read is performed.

However, the standard does require that a reference must be initialized to refer to a valid object or function2 (though this is subject to CWG 453), so things like E f(*reinterpret_cast<E*>(nullptr)); will be ill-formed.


1. This is done by not normatively requiring such conversion, and further strengthened by the non-normative note in [dcl.init.ref].

2. [dcl.ref].