5

After seeing that a local reference-to-const may prolong the life of a temporary, I encountered the need to conditionally bind a local reference-to-const to either a function parameter or the temporary result of a function call, i.e.:

class Gizmo
{
    // Rule of Five members implemented
};

Gizmo Frobnicate(const Gizmo& arg);

void ProcessGizmo(const Gizmo& arg, bool frobnicate)
{
    const Foo& local = frobnicate ? Frobnicate(arg) : arg;
    // Perform some work on local
}

A practical example: the boolean specifies whether to compress a buffer and you'd like to write uniform code that operates on local either way.

The above example, however, invoked Gizmo's copy-constructor on arg when frobnicate was false. I managed to avoid the invocation of the copy-constructor by changing Frobnicate(arg) to static_cast<const Gizmo&>(Frobnicate(arg)).

My question becomes: how does the ternary operator interact with the rule about binding a local reference-to-const to a temporary? Is my solution legal and well-behaved?

R Sahu
  • 204,454
  • 14
  • 159
  • 270
DoomMuffins
  • 1,174
  • 1
  • 9
  • 19

1 Answers1

5

No, it's not well-behaved. For the lifetime extension to occur, the temporary must be bound directly to the reference. Once you add a cast, the binding is no longer direct, and the resulting reference becomes dangling.

We can see this with some simple testing code:

#include <iostream>
struct Gizmo
{
    ~Gizmo() { std::cout << "Gizmo destroyed\n"; }
};

Gizmo Frobnicate(const Gizmo& arg) { return arg; }

void ProcessGizmo(const Gizmo& arg, bool frobnicate)
{
    const Gizmo& local = frobnicate ? static_cast<const Gizmo&>(Frobnicate(arg)) : arg;
    // Perform some work on local
    (void) local;
    std::cout << "Processing\n";
}

int main(){
    Gizmo g;
    ProcessGizmo(g, true);
    std::cout << "Processed\n";
}

This prints:

Gizmo destroyed
Processing
Processed
Gizmo destroyed

That first Gizmo destroyed message is from the return value of Frobnicate(); it's being destroyed at the end of that line - without lifetime extension.

An obvious workaround is moving the processing into another function:

void DoProcessGizmo(const Gizmo& arg) { /* process the Gizmo */ }

void ProcessGizmo(const Gizmo& arg, bool frobnicate)
{
    return frobnicate ? DoProcessGizmo(Frobnicate(arg)) : DoProcessGizmo(arg);
}
T.C.
  • 133,968
  • 17
  • 288
  • 421
  • What about "return DoProcessGizmo( frobnicate ? Frobnicate(arg) : arg );" This also seems to work as expected – dats Oct 02 '18 at 14:43
  • That makes a copy that the OP is seeking to avoid. – T.C. Oct 02 '18 at 20:20
  • Hum... you are right! But I'm pretty sure I've seen that copy avoided in some version of MSVC in the past... Not that I can reproduce it with a recent version though. Thanks! I have to go and avoid some copies now! – dats Oct 04 '18 at 08:56
  • What about `return DoProcessGizmo( frobnicate ? static_cast< const Gizmo & >( Frobnicate( arg ) ) : arg );`? I assume this is legal C++, with no undefined behavior (albeit uglier than having two independent calls to `DoProcessGizmo`). – jchl Jul 31 '20 at 11:18