15

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.
Community
  • 1
  • 1
Passer By
  • 19,325
  • 6
  • 49
  • 96
  • @user2079303 Specifically, the linked question – Passer By Jan 19 '18 at 13:21
  • Oh, thanks. I missed the first link. – eerorika Jan 19 '18 at 13:22
  • It feels safe to say that copying it is going to count as a read. – David Hoelzer Jan 19 '18 at 13:22
  • Would it imply that `E* pe = nullptr; E e{*pe};` is legal? [gcc accept it anyway](http://coliru.stacked-crooked.com/a/a1f5e684a2745977). – YSC Jan 19 '18 at 13:22
  • 1
    @YSC the evaluation of `*pe` by itself is UB right away, so no. – Quentin Jan 19 '18 at 13:23
  • @Quentin yes obviously. I was too quick about it. – YSC Jan 19 '18 at 13:27
  • I know that is not the question (which I upvoted because I think is interesting), and maybe this is obvious, but even if it turns out it does not count as an access, writing code relying on this assumption seems like a terrible idea. – jdehesa Jan 19 '18 at 13:39
  • 2
    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 Jan 19 '18 at 13:40
  • 2
    @UKMonkey But the copy constructor looks like its defined to do nothing, the single byte is probably considered padding – Passer By Jan 19 '18 at 13:43
  • @PasserBy looks like unspecified behavior actually; nothing is said about the padding of the class at all. – UKMonkey Jan 19 '18 at 13:44
  • @UKMonkey note the difference between the definition of the default copy/move constructor between a struct (quoted in question) and an union [_"The implicitly-defined copy/move constructor for a union X copies the object representation of X."_](https://timsong-cpp.github.io/cppwp/class.copy.ctor#15). I agree with the reading of @PasserBy: the copy/move constructor do nothing. But does is counts as not accessing `e`? The question could be generalized to a function taking an argument without doing anything of it. – YSC Jan 19 '18 at 13:45
  • 1
    @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 Jan 19 '18 at 14:03
  • 1
    @Columbo did you? Is there a link I could follow? – YSC Jan 19 '18 at 14:04
  • 1
    @YSC For example, [this](https://stackoverflow.com/questions/28482809/c-access-static-members-using-null-pointer/28483256#28483256), but Richard Smith made a similar comment someplace else. – Columbo Jan 19 '18 at 14:05
  • @Columbo ouch, I don't have enough brain for that right now. But do you mean that the `const&` parameter of the implicit copy constructor does trigger UB by forming an actual "null reference"? – Quentin Jan 19 '18 at 14:10
  • @Quentin Sounds right. – Columbo Jan 19 '18 at 14:15
  • @Columbo I thought "null reference" lead to UB only when odr-used. Was I wrong? – YSC Jan 19 '18 at 14:18
  • @YSC [Yes](http://eel.is/c++draft/dcl.ref#5.sentence-3). – Columbo Jan 19 '18 at 14:19
  • 1
    @Columbo Thank you for your time. – YSC Jan 19 '18 at 14:24
  • What do you want to know? 1) Whether it's an access in the abstract machine? No, see answers. 2) Whether a reference to invalid can be passed? No, because making the reference [causes UB](https://stackoverflow.com/questions/4364536/is-null-reference-possible#4364586). 3) Whether any padding will ever get copied? Possibly, see my `memcpy` comment below. 4) Whether data races are possible when `e` is assigned concurrently? No, because padding is not a [memory location](http://en.cppreference.com/w/cpp/language/memory_model). Ending `e`'s lifetime is probably not allowed, though. – Arne Vogel Jan 19 '18 at 17:19
  • @Columbo That assumes the direction of CWG 232, no? With the text as it stands, I'd argue that it's undefined by omission. – T.C. Jan 19 '18 at 17:39
  • @T.C. The text is defective. Richard has mentioned that the committee has simply no time to deal with this at the moment. (This was on std-discussion, I could find my response linking to my draft on empty lvalues if you wish.) – Columbo Jan 19 '18 at 18:23

2 Answers2

3

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.

Oliv
  • 17,610
  • 1
  • 29
  • 72
2

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].

cpplearner
  • 13,776
  • 2
  • 47
  • 72
  • 1
    Wow, easy there. `static_cast` is more than enough :) – Quentin Jan 19 '18 at 14:23
  • @cpplearner I guess the question relates more to reading or writing from the memory taken by `e`. `sizeof(e)` is greater than zero (probably 1). I doubt that the standard prohibits accessing padding bytes in any case as long as there are no ill consequences e.g. due to data races. Consider `struct S{ double d; char c[7]; E e; };` – this is trivially copyable and the compiler will probably use `memcpy` to copy an array of `S`. This implies the padding in the source `e`s are read from, and written to in the destination `e`s. It makes no sense to forbid that. – Arne Vogel Jan 19 '18 at 14:32
  • 3
    @ArneVogel That does not constitute an access in the [abstract machine](http://eel.is/c++draft/intro.abstract). – cpplearner Jan 19 '18 at 14:35
  • 1
    @Quentin Actually, you can't use a `reinterpret_cast` for that conversion at all. – T.C. Jan 19 '18 at 17:40