Does the C++ standard guarantee that a function return value has a constant address?
Asked Answered
I

2

16

Consider this program:

#include <stdio.h>
struct S {
  S() { print(); }
  void print() { printf("%p\n", (void *) this); }
};
S f() { return {}; }
int main() { f().print(); }

As far as I can tell, there is exactly one S object constructed here. There is no copy elision taking place: there is no copy to be elided in the first place, and indeed, if I explicitly delete the copy and/or move constructor, compilers continue to accept the program.

However, I see two different pointer values printed. This happens because my platform's ABI returns trivially copyable types such as this one in CPU registers, so there is no way with that ABI of avoiding a copy. clang preserves this behaviour even when optimising away the function call altogether. If I give S a non-trivial copy constructor, even if it's inaccessible, then I do see the same value printed twice.

The initial call to print() happens during construction, which is before the start of the object's lifetime, but using this inside a constructor is normally valid so long as it isn't used in a way that requires the construction to have finished -- no casting to a derived class, for instance -- and as far as I know, printing or storing its value doesn't require the construction to have finished.

Does the standard allow this program to print two different pointer values?

Note: I'm aware that the standard allows this program to print two different representations of the same pointer value, and technically, I haven't ruled that out. I could create a different program that avoids comparing pointer representations, but it would be more difficult to understand, so I would like to avoid that if possible.

Iyeyasu answered 26/6, 2016 at 21:19 Comment(12)
Curious. With -fno-elide-constructors g++ still gives different results but the addresses are the same in clangAnachronistic
@Anachronistic No they're not, but b and d do look alike. :)Iyeyasu
Oh jeez. Can't believe I didn't see that. I'll go sit in the corner now ;)Anachronistic
This is CWG 1590. Probably resolved by P0135R1, but we'll have to wait a couple weeks to see the actual paper.Criner
When you declare a copy constructor it puts out the same address. ideone.com/KKR0SE I'm not sure why though.Marquardt
@T.C.It seems you need to be a member of the standard committee to open your link. Is there a public/anonymous read-only access?Duodenary
@Criner Thanks, that link requires a login but this one doesn't. I'm worried; GCC and clang's current behaviour would not be allowed by the proposed resolution.Iyeyasu
@Marquardt I hinted at that in my question already. It's because the ABI makes a difference between trivially copyable and non-trivially-copyable types. Your change makes the type non-trivially-copyable.Iyeyasu
@Duodenary Ugh, they changed wg21.link to be less useful for us mere plebs. :( Use hvd's link.Criner
@hvd The only currently publicly available indication of P0135R1's content I know of is a draft in Richard Smith's github repository. If I'm reading the [class.temporary] changes correctly, it should allow the behavior you observe.Criner
@Criner It includes "and X has at least one non-deleted copy or move constructor", but GCC and clang perform a copy even if all copy and move constructors are deleted, so it wouldn't be enough. Thanks for finding that, feel free to post it as an answer, or I'll try to find a way of getting it into a decent answer myself later.Iyeyasu
This behavior is really weird, this may lead to some issues if the code rely on this's address?Composer
I
5

T.C. pointed out in the comments that this is a defect in the standard. It's core language issue 1590. It's a subtly different issue than my example, but the same root cause:

Some ABIs require that an object of certain class types be passed in a register [...]. The Standard should be changed to permit this usage.

The current suggested wording would cover this by adding a new rule to the standard:

When an object of class type X is passed to or returned from a function, if each copy constructor, move constructor, and destructor of X is either trivial or deleted, and X has at least one non-deleted copy or move constructor, implementations are permitted to create a temporary object to hold the function parameter or result object. [...]

For the most part, this would permit the current GCC/clang behaviour.

There is a small corner case: currently, when a type has only a deleted copy or move constructor that would be trivial if defaulted, by the current rules of the standard, that constructor is still trivial if deleted:

12.8 Copying and moving class objects [class.copy]

12 A copy/move constructor for class X is trivial if it is not user-provided [...]

A deleted copy constructor is not user-provided, and nothing of what follows would render such a copy constructor non-trivial. So as specified by the standard, such a constructor is trivial, and as specified by my platform's ABI, because of the trivial constructor, GCC and clang create an extra copy in that case too. A one-line addition to my test program demonstrates this:

#include <stdio.h>
struct S {
  S() { print(); }
  S(const S &) = delete;
  void print() { printf("%p\n", (void *) this); }
};
S f() { return {}; }
int main() { f().print(); }

This prints two different addresses with both GCC and clang, even though even the proposed resolution would require the same address to be printed twice. This appears to suggest that while we will get an update to the standard to not require a radically incompatible ABI, we will still need to get an update to the ABI to handle the corner case in a manner compatible with what the standard will require.

Iyeyasu answered 28/6, 2016 at 19:29 Comment(1)
Also, this thread on cxx-abi-dev is interesting.Criner
I
1

This is not an answer, rather a note on the different behavior of g++ and clang in this case, depending on the -O optimization flag.

Consider the following code:

#include <stdio.h>
struct S {
    int i;
    S(int _i): i(_i) { 
        int* p = print("from ctor");
        printf("about to put 5 in %p\n", (void *)&i);        
        *p = 5;
    }
    int* print(const char* s) { 
        printf("%s: %p %d %p\n", s, (void *) this, i, (void *)&i);
        return &i;
    }
};
S f() { return {3}; }
int main() { 
    f().print("from main");
}

We can see that clang (3.8) and g++ (6.1) are taking it a bit differently, but both get to the right answer.

clang (for no -O, -O1, -O2) and g++ (for no -O, -O1)

from ctor: 0x7fff9d5e86b8 3 0x7fff9d5e86b8
about to put 5 in 0x7fff9d5e86b8
from main: 0x7fff9d5e86b0 5 0x7fff9d5e86b0

g++ (for -O2)

from ctor: 0x7fff52a36010 3 0x7fff52a36010
about to put 5 in 0x7fff52a36010
from main: 0x7fff52a36010 5 0x7fff52a36010

It seems that they both do it right in both cases - when they decide to skip the register optimization (g++ -O2) and when they go with the register optimization but copy the value to the actual i on time (all other cases).

Incivility answered 28/6, 2016 at 9:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.