0

Can someone tell me if this is safe, because I think it isn't:

class A { public: A(int*& i) : m_i(i) {} int*& m_i; }; class B { public: B(int* const& i) : m_i(i) {} int* const & m_i; }; int main() { int i = 1; int *j = &i; A a1(j); // this works (1) A a2(&i); // compiler error (2) B b(&i); // this works again (3) } 

I understand why (1) works. We are passing a pointer, the function accepts it as a reference.

But why doesn't (2) work? From my perspective, we are passing the same pointer, just without assigning it to a pointer variable first. My guess is that &i is an rvalue and has no memory of its own, so the reference cannot be valid. I can accept that explanation (if it's true).

But why the heck does (3) compile? Wouldn't that mean that we allow the invalid reference so b.m_i is essentially undefined?

Am I completely wrong in how this works? I am asking because I am getting weird unit test fails that I can only explain by pointers becoming invalid. They only happen for some compilers, so I was assuming this must be something outside the standard.

So my core question basically is: Is using int* const & in a function argument inherently dangerous and should be avoided, since an unsuspecting caller might always call it with &i like with a regular pointer argument?

Addendum: As @franji1 pointed out, the following is an interesting thought to understand what happens here. I modified main() to change the inner pointer and then print the members m_i:

int main() { int i = 1; int *j = &i; // j points to 1 A a1(j); B b(&i); int re = 2; j = &re; // j now points to 2 std::cout << *a1.m_i << "\n"; // output: 2 std::cout << *b.m_i << "\n"; // output: 1 } 

So, clearly a1 works as intended. However, since b cannot know that j has been modified, it seems to hold a reference to a "personal" pointer, but my worry is that it is not well defined in the standard, so there might be compilers for which this "personal" pointer is undefined. Can anyone confirm this?

3
  • In a1, m_i reference is of j. If further down, you re-assigned j to point to, say &k, then a1.m_i would be pointing to k. What specific pointer is a2 referencing? Commented May 6, 2020 at 18:04
  • @franji1 I think you are trying to point me towards why (2) does not work and the explanation is straightforward to understand. Thanks. But it still does not explain why (3) works. I think this is the core of my question. My assumption is that according to your explanation a2.m_i is not really referencing anything, or at least something undefined, and that is why I wanted confirmation that this construct is essentially dangerous. Commented May 6, 2020 at 18:18
  • B b(&i); is no more dangerous than A a1(j); Both (1) and (3) can become dangling if the lifetime of i and j is less than a1 and b. Difference between (2) and (3) is that &i is not a valid reference to int*, but it is valid const reference to int*, that's all. Commented May 6, 2020 at 19:00

3 Answers 3

2

A's constructor takes a non-const reference to an int* pointer. A a1(j); works, because j is an int* variable, so the reference is satisfied. And j outlives a1, so the A::m_i member is safe to use for the lifetime of a1.

A a2(&i); fails to compile, because although &i is an int*, operator& returns a temporary value, which cannot be bound to a non-const reference.

B b(&i); compiles, because B's constructor takes a reference to a const int*, which can be bound to a temporary. The temporary's lifetime will be extended by being bound to the constructor's i parameter, but will then expire once the constructor exits, thus the B::m_i member will be a dangling reference and not be safe to use at all after the constructor has exited.

Sign up to request clarification or add additional context in comments.

13 Comments

That's exactly what I thought. Thanks for providing the terminology and clearing it up. Could you shed some light on whether using a function parameter of int* const& is inherently bad (and only accepted in the language for completeness) or if there are actual practical uses? Because I think it's asking quite a bit from the caller to assume that calling the function with a &i is a bad idea while it would not be a problem if the function only accepted a pointer without the reference.
There is nothing inherently bad with using an int* const & parameter itself (though, the real question is, why are you using pointers by reference to begin with, and not int& instead?). But you are taking it a step further by trying to save what the reference refers to, and that is dangerous when what is being referred to is a temporary value. A reference does not know what it is referring to. Saving a reference of another reference is legal from the compiler's perspective, but is questionable depending on the context.
The architecture I am using forces me to provide references to structures that are filled at a later point in time. Since one of these structures is in fact a pointer, I need to pass a reference to a pointer (although a pointer to pointer would work as well). I have to admit that I was a bit out of my depth about const-ness of pointers so I tried different ways until it compiled. That's what I get for not fully understanding the mechanism before using it I guess.
@RiadBaghbanli you are incorrect for the reasons shown by both me and Remy Lebeau: &i from B b(&i); is a prvalue - a temporary. This temporary ends on the constructor end. The m_i is now left a dangling reference.
@ceno why not just put the tasks into a queue, then start the 1st task and let it start the 2nd task before exiting, passing its completed data (ie, the selected pointer) as input at that time? Then that 2nd task can start the 3rd task, giving it input as needed. And so on. You don't need to hook up the individual tasks to each other, or references to some external shared data. Just pass the relevant data down the chain, one task at a time.
|
1

j is an lvalue and as such it can be bound to a non-const lvaue reference.

&i is a prvalue and it cannot be bound to non-const lvalue reference. That's why (2) doesn't compile

&i is a prvalue (a temporary) and it can be bound to a const lvalue reference. Bounding a prvalue to a reference extends the lifetime of the temporary to the lifetime of the reference. In this case this temporary lifetime is extended to the lifetime of the constructor parameter i. You then initialize the reference m_i to i (constructor parameter) (which is a reference to the temporary) but because i is an lvalue the lifetime of the temporary is not extended. In the end you end up with a reference member m_i bound to an object which is not alive. You have a dangling reference. Accessing m_i from now on (after the constructor has finished) is Undefined Behavior.


Simple table of what can references bind to: C++11 rvalue reference vs const reference

16 Comments

Thanks for the very good explanation. This would explain why using the reference to a const pointer in a function may work without problems, but this case fails because I copy it to a member afterwards. So is it possible that some compilers extend the lifetime of the constructor parameter to the lifetime of the member? Because I only had problems in one specific case. In C++ shell for example, this worked, so I assume their compiler implements this according to my original intuition (which turned out to be wrong in general).
@Cerno no. Undefined Behavior is ... well.. undefined. Anything can happen. It can appear to work, it can crash it can do any crazy stuff.
"So is it possible that some compilers extend the lifetime of the constructor parameter to the lifetime of the member?" - no, because that is not how lifetime extension is defined to work.
@RemyLebeau Thanks, so, I take it's just coincidence that it worked, as in, some freed memory section may still contain the temporary pointer address so I can still access it but it's not guaranteed, or something similar?
@Cerno No it is not coincidence. Difference between (2) and (3) is that &i is not a valid reference to int*, but it is valid const reference to int*. This answer is not accurate.
|
-1

Pointer is a memory address. For simplicity, think of a pointer as uint64_t variable holding a number representing the memory address of whatever. Reference is just a alias for some variable.

In example (1) you are passing a pointer to constructor expecting a reference to pointer. It works as intended, as compiler gets the address of memory where the value of pointer is stored and passes it to constructor. The constructor gets that number and creates an alias pointer. As a result you are getting an alias of j. If you modify j to point to something else then m_i will also be modified. You can modify m_i to point to something else too.

In example (2) you are passing a number value to the constructor expecting a reference to pointer. So, instead of an address of an address, constructor gets an address and compiler has no way to satisfy the signature of the constructor.

In example (3) you are passing a number value to constructor expecting a constant reference to pointer. Constant reference is a fixed number, just a memory address. In this case compiler understands the intent and provides the memory address to set in the constructor. As a result you are getting fixed alias of i.

EDIT (for clarity): Difference between (2) and (3) is that &i is not a valid reference to int*, but it is a valid const reference to int*.

8 Comments

I am not sure this is correct. If (3) is valid then why does my unit test fail? Any why only for certain compilers? This smells like undefined behaviour to me.
Your unit test demonstrates the exact behavior expected as per C++ specs. b.m_i is a reference to i, not j. Modifying j will have no effect on b.m_i. If you are to modify the value of i', then b.m_i` will be modified also.
are you saying that (3) does not end up with a dangling reference?
(3) is no more dangling than (1). b.m_i references i, not temporary variable. It passes const value &i as a constant reference. Check it for yourself in debugger.
from you answer in case (3): "but it is a valid const reference to int*" no, it's not not. It's a reference to the &i temporary created on B b(&i);. This temporary ends on when the constructor of b ends and b.m_i is now a reference to an expired temporary. A dangling referece.
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.