3

I've been working on a project that using good amount of double pointers and I was finding I was getting some bugs with it. After spending some time digging into it I've realised the issue is when you pass a non-const object via a reference of a pointer to a const object it ends up getting passed by copy instead of reference. I don't understand why this would be the case, since a reference is just an alias and the const part should just cause an error if you try to change something about the object in that scope. To further this confusion this does not happen when passing by a double-pointer of a const object or when it's just a reference to a const object. Can anyone explain why this is happening and how this particular case is different from the others I've included?

#include <iostream>

void PassByConstPtrRef(int const *const &num_ptr_ref)
{
    std::cout << &num_ptr_ref << std::endl;
}

void PassByPtrRef(int *const &num_ptr_ref)
{
    std::cout << &num_ptr_ref << std::endl;
}

void PassByPtrPtr(int const *const *const num_ptr_ref)
{
    std::cout << num_ptr_ref << std::endl;
}

void PassByConstRef(int const &num)
{
    std::cout << &num << std::endl;
}

int main()
{
    int *num_ptr = new int{ 10 };
    int *const *num_ptr_ptr = &num_ptr;
    int *const &num_ptr_ref = *num_ptr_ptr;

    std::cout << num_ptr_ptr << " : " << &num_ptr_ref << std::endl; // is equal

    std::cout << num_ptr_ptr << " : ";
    PassByConstPtrRef(num_ptr_ref); // is not equal

    std::cout << num_ptr_ptr << " : ";
    PassByPtrRef(num_ptr_ref); // is equal

    std::cout << num_ptr_ptr << " : ";
    PassByPtrPtr(&num_ptr_ref); // is equal

    int foo = 4;
    int &bar = foo;

    std::cout << &foo << " : ";
    PassByConstRef(bar); // is equal
}

output :

0x7ffeeb6d59c8 : 0x7ffeeb6d59c8

0x7ffeeb6d59c8 : 0x7ffeeb6d59b0

0x7ffeeb6d59c8 : 0x7ffeeb6d59c8

0x7ffeeb6d59c8 : 0x7ffeeb6d59c8

0x7ffeeb6d59ac : 0x7ffeeb6d59ac

  • @churill Thanks, I've added the output at the bottom. – Joshua Williams Mar 05 '21 at 19:46
  • @JaMiT I'm happy to receive corrections on my speculation if you have any? It would probably make it less confusing for me. I was just trying to figure it out myself before I came here to ask questions so I thought I should share where I was at. – Joshua Williams Mar 05 '21 at 19:51
  • @JaMiT wouldn't that make my code work for int but not classes? It doesn't seem to work in either case and I've got my original code working by removing the const. – Joshua Williams Mar 05 '21 at 19:53
  • 1
    Ah... I see one reason there is confusion. The results changed between gcc 9 and gcc 10, and between clang 9 and clang 10. People testing in the 10+ versions would not see the discrepancy. – JaMiT Mar 05 '21 at 20:32
  • @JaMiT Yep, exactly that was my problem. MSVC is also producing OPs results. Here's a [test on godbolt](https://godbolt.org/z/ea5hrY) for reference, showing the 2 compilers in different versions. – churill Mar 05 '21 at 20:40

2 Answers2

2

This is a confusing one. The immediate cause of the symptom is that in order to pass num_ptr_ref to PassByConstPtrRef() a temporary was created. The parameter bound to this temporary, hence had a different address. While this phenomenon is a (somewhat) known hazard when dealing with objects, it does seem strange for it to occur with pointers when the only difference is a more stringent cv-qualification ("cv" is short for "const-volatile"). After all, the data is the same; the difference is what can be done with the data.

A deeper cause of this is the wording used in the C++17 standard. The relevant section ([dcl.init.ref]) used the phrase "the same type as" and int const * is not the same type as int *. There was an allowance made for different cv-qualification at the top level of a type, but not at deeper levels. As a result, the standard actually required the creation of a temporary in your case. (Past tense is intentional.)

Fortunately, this situation was recognized as undesirable, as documented in defect report 2352. The proposed resolution was to change "the same type as" to "similar", which does account for cv-qualification of pointed-to types. There were other wording changes as well. As I read it, the other changes are either consequences of switching to "similar" or tidying up the phrasings to reduce the chance of future defects. Perhaps the other changes are more significant than I realize, but the important point is that with these changes, a temporary is no longer required in your situation.

The resolution to the defect was incorporated into GCC 10 and clang 10, but not into earlier versions. (I don't know which, if any, version of MSVC has this enhancement.) Older compilers will give your result (different addresses), while newer compilers will not. Consider this a reason to upgrade? :)


For reference, here is the example given in the defect report. It makes use of a returned value instead of a parameter, but the principle is the same. There is a pointer to int and a reference to a (const) pointer to const int, and the reference cannot bind directly to the pointer.

In an example like

int *ptr;
const int *const &f() {
  return ptr;
}

What is returned is a reference to a temporary instead of binding directly to ptr.

JaMiT
  • 9,693
  • 2
  • 12
  • 26
  • It's a little bit of a tough read but I think I've got the gist. Thanks for the explanation pointing me to the right resources. – Joshua Williams Mar 05 '21 at 23:40
  • @JoshuaWilliams I finally got back to this. The answer should be an easier read now. (I'm not promising "easy", but "easier". ;) ) – JaMiT Mar 08 '21 at 02:36
0

Your problem puzzled me so I investigated on it.

Basically it can be boiled down to this code for illustration:

#include <iostream>

int main()
{
    int *num_ptr = new int{ 10 };
    int *const &num_ptr_ref = num_ptr;
    int *const *num_ptr_ptr = &num_ptr;

    // Using a reference
    int const *const &const_num_ptr_ref = num_ptr_ref;
    std::cout << num_ptr_ptr << " : " << &const_num_ptr_ref << std::endl;;

    // Using a pointer    
    int const *const *const const_num_ptr_ptr = num_ptr_ptr;
    std::cout << num_ptr_ptr << " : " << const_num_ptr_ptr << std::endl;;
}

When trying it on https://www.onlinegdb.com/online_c++_compiler, you can see that the first 2 addresses are different, while the last 2 are the same.

After some digging here is my understanding of the situation:

  1. The first assignation is a reference initialization (https://en.cppreference.com/w/cpp/language/reference_initialization)
  2. The second assignation is a multi-level pointer qualification conversion (https://en.cppreference.com/w/cpp/language/implicit_conversion#Qualification_conversions)

The relevant difference to me here is that during a reference binding, if the type of the reference and the assigned expression are not the same it can involve "materializing a temporary if necessary" (https://en.cppreference.com/w/cpp/language/implicit_conversion#Temporary_materialization), which clearly seems to be happening here.

The different relevant sections of cppreference I linked do kind of cross-reference each other so its hard to be completely sure I haven't missed something but it seems to explain the behavior.

Drax
  • 11,247
  • 5
  • 37
  • 77