1

I'm getting very weird results from ternary operator that should return reference to member object of a linked list. I have member function in my database object like this:

Month& GetLastMonth() { return months_.Size() > 0 ? months_.Last() : Month(); }

I call this function 2 times in code: right after loading DB to get info, and when changing some parameters of month and pushing a button.

First time I write it to const reference, no big deal here. But second time I need non-const reference to recalculate month (so it will change in DB too).

But instead of expected results, some black magic happens. Every time I call GetLastMonth() function it returns a reference with different address. So, after DB loads - its 1 address, recalculating month - its 2nd address and right after saving to file its 3rd address. Of course, because of this, my changes after recalculating month are not saved to file.

Now, size of months_ are always 1 (for testing purposes). Also it's linked list, so Month can never reallocate. I've tested it, and it never calls Month(), so seems like ternary operator is working fine.

Calling it like this, gives same result:

Month& GetLastMonth() { return months_.Size() ? months_.Last() : Month(); }

Howerver if I call it without ternary:

Month& GetLastMonth() { return months_.Last(); }

Or with proper if():

Month& GetLastMonth()
{ 
    if(months_.Size() > 0)
    {
        return months_.Last();
    }
    return Month();
}

It works as expected and returns reference with same address all 3 times. I was thinking about this obscure thing for about 2 days, but still can't find any reasoning behind this.

Edit: Related question: Return type of '?:' (ternary conditional operator)

ScienceDiscoverer
  • 205
  • 1
  • 3
  • 13
  • 3
    Note that returning `Month()` is a MS specific extension (because it would bind to a non-const lvalue ref). It doesn't work in standard C++. – Rakete1111 Oct 01 '18 at 16:49
  • 1
    You should probably change the return type to `Month`, not `Month&` as you're returning a temporary. – tadman Oct 01 '18 at 16:52
  • 1
    @tadman OP wants the non-const reference though. – Rakete1111 Oct 01 '18 at 16:54
  • @Rakete1111 So, if not for the Microsoft, I wouldn't waste 2 days on this bug? Thanks, Bill! – ScienceDiscoverer Oct 01 '18 at 17:21
  • 1
    @tadman Yea, I see now that it is really a bad idea to return temporal object as reference... I'll try to refactor code a bit, and think up some better way to guard from accessing empty list... – ScienceDiscoverer Oct 01 '18 at 17:22
  • 2
    A more C++ approach is to return an iterator that might be equal to `months.end()`, e.g. no entry found, or to throw an exception. If this is a container, making it conform to C++ container expectations and standards is usually a good idea. – tadman Oct 01 '18 at 18:08

1 Answers1

3

The ternary operator finds a common type and value category of both operands, and this doesn't happen for the two other cases, which is why it works there.

months_.Last() and Month() both have type Month, so everything is fine there. But now, let's examine the value categories. months_.Last() is an lvalue, while Month() is a prvalue! So, the common value category here is a prvalue, and both operands get converted to a prvalue. Which means that you get a new Month from months_.Last() every time (from the copy)!

Note again however, that this is a MS specific extension. Without that extension, the code would be invalid, as you would try to bind the prvalue returned by the conditional operator to a non-const lvalue reference.

Rakete1111
  • 47,013
  • 16
  • 123
  • 162
  • Wow... I had no idea that prvalue existed and also 0 idea about such specific behavior of ternary operator... Need to study this topics in depth... Now, I'm certainly will try to be more careful with ternary-es... – ScienceDiscoverer Oct 01 '18 at 17:24
  • So, If I understood correctly, ternary just physically can't convert prvalue to lvalue, so it does what it can do - converts lvalue to prvalue by copying it? – ScienceDiscoverer Oct 01 '18 at 17:30
  • @ScienceDiscoverer There is a conversion from prvalue to lvalue, but it doesn't happen here (for the ternary). Yes. That conversion is called lvalue-to-rvalue conversion, and does indeed copy the object if it is a class. – Rakete1111 Oct 01 '18 at 17:51
  • It's some specific rule for ternary operator to prefer lvalue-to-rvalue conversions? – ScienceDiscoverer Oct 01 '18 at 18:02
  • @ScienceDiscoverer Yes. (The other conversion is not even considered). That way it works nicely with guaranteed copy elision (no need to create an object in the ternary if none is needed - prvalues are not objects). – Rakete1111 Oct 01 '18 at 21:48