Temporary object creation for reference parameter and optimization changes
Asked Answered
S

1

6

I have the following code

#include <iostream>

void foo(const int* const &i_p) {
    std::cout << &i_p << std::endl;
}

int main () {

    int i = 10;
    int* i_p = &i;
    std::cout << &i_p << std::endl;
    foo(i_p);
}

On x86-64 clang 9.0.0 the output is

Program returned: 0
0x7ffc43de63f8
0x7ffc43de6400

while on x86-64 clang 10.0.0 Compiler Explorer link the output becomes

Program returned: 0
0x7ffc9da01ef0
0x7ffc9da01ef0

What optimization is at play here that gives the same address? I believe a temporary object should materialize since we cannot bind low-level const pointer to low-level non-const.

Sartin answered 15/9, 2023 at 17:30 Comment(6)
Given that foo() cannot actually modify its parameter, clang is smart enough to hijack main()'s stack frame and recycle it in foo()? All references themselves are, by definition const, for all practical matters. It should be interesting if the same optimization works for a non-reference, but a const parameter.Consider
With a few additional minutes' worth of thought, this cannot be a valid optimization in some cases. Here, the compiler can theoretically prove, thanks to LTO, that there are no other execution threads. But in a code that uses multiple execution threads, another execution thread can modify i_p anytime, resulting in this optimization being able to observe it, in foo(). Fail.Consider
Several objects of different types might have same address. In particular, [[no_unique_address]] has been added in C++20 so associated optimization code might be used more widely?Romito
I tried to reproduce this with multiple environments and optimizations and I cannot replicate the 8 byte shift you are seeing with clang 9.0.0. Does this consistently happen? I wonder if it is a byte boundary issue considering the shift is less than the size of an int. Also does this happen if you pack the entire int? ex. int I = 0xffffffff;Casket
@SamVarshavchik That optimization would not be permitted in any situation. The lifetime of i_p in main ends only after the function call. So i_p in main and a temporary object bound to the function parameter (if there was one) are not allowed to have the same address, because their lifetimes overlap.Dubenko
@Romito Two (non-bitfield) objects that have overlapping lifetime may only have the same address if one is nested within the other or one is a subobject of zero size (+ further restrictions).Dubenko
T
4

What optimization is at play here that gives the same address? I believe a temporary object should materialize since we cannot bind low-level const pointer to low-level non-const.

There is no trick:

  • it is valid to convert an int* to const int*, and
  • it is valid to convert an int** to a const int * const *, however
  • it is not valid to convert int** to const int** (but that case isn't relevant here).

See Why isn't it legal to convert "pointer to pointer to non-const" to a "pointer to pointer to const".

Current wording

You only have a single i_p pointer, and naturally, both &i_p in main and &i_p in foo should yield the same address. foo accepts a reference to a pointer, so it should refer to that in main.

const int * const is reference-compatible with int * because a a pointer to int*, i.e. int** could be converted to const int * const* via a qualification conversion. Due to this, a reference to const int * const can bind to int*.

There is nothing which would necessitate the creation of a second object, at least not in the current draft. const int& can bind to int without creating a separate object, and the same principle applies to your case.

Historical defects explaining your observed behavior

However, it didn't always use to be like that, and qualification conversions haven't always been considered during reference binding. CWG2018. Qualification conversion vs reference binding points out that qualification conversions aren't considered and temporary objects are created, such as in this example:

const int &r1 = make<int&>();           // ok, binds directly
const int *const &r2 = make<int*&>();   // weird, binds to a temporary
const int *&r3 = make<int*&>();         // error

const int &&x1 = make<int&&>();         // ok, binds directly
const int *const &&x2 = make<int*&&>(); // weird, binds to a temporary
const int *&&x3 = make<int*&&>();       // weird, binds to a temporary

Note: there is still some compiler divergence; GCC considers x3 to be ill-formed, and clang performs temporary materialization.

The example r2 is exactly your case. If the const int* const &i_p isn't simply binding to the i_p in main but creates a brand new temporary pointer when foo is called, then it would be a separate object. Note that const& can bind to temporary objects thanks to temporary materialization. If there is a separate temporary object which i_p in foo binds to, then it would also need to have a separate address from that in main.

This behavior is a defect though, fixed by CWG2352: Similar types and reference binding. The new wording was implemented in GCC 7 and clang 10, which is why you no longer see two separate pointers being created.

Tanatanach answered 15/9, 2023 at 17:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.