Search code examples
c++language-lawyerclang++const-correctnessreference-binding

Temporary object creation for reference parameter and optimization changes


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.


Solution

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