19

Consider the paradigmatic max template function, std::max():

// From the STL

    // TEMPLATE FUNCTION _Debug_lt
template<class _Ty1, class _Ty2> inline
    bool _Debug_lt(const _Ty1& _Left, const _Ty2& _Right,
        _Dbfile_t _File, _Dbline_t _Line)
    {   // test if _Left < _Right and operator< is strict weak ordering
        if (!(_Left < _Right))
            return (false);
        else if (_Right < _Left)
            _DEBUG_ERROR2("invalid operator<", _File, _Line);
        return (true);
    }

// intermediate #defines/templates skipped 

    // TEMPLATE FUNCTION max
template<class _Ty> inline
    const _Ty& (max)(const _Ty& _Left, const _Ty& _Right)
    {   // return larger of _Left and _Right
        return (_DEBUG_LT(_Left, _Right) ? _Right : _Left);
    }

... Or, in simpler form:

template<typename T> inline
T const & max(T const & lhs, T const & rhs)
{
    return lhs < rhs ? rhs : lhs;
}

I understand why the max template must return by reference (to avoid expensive copies and to avoid the requirement for a copy constructor).

However, doesn't this lead to the possibility of dangling references, as in the following code?

int main()
{
    const int & max_ = ::max(3, 4);
    int m = max_; // Is max_ a dangling reference?
}

In this case, the code builds and runs fine (VS 2010) and the value m is set to 4. However, it strikes me that max_ is a dangling reference since the hard-coded rvalues 3 and 4 were passed directly to max, and the storage allocated for these hard-coded rvalue constants can rightfully be de-allocated by the time the following line of code is reached.

I could envision analogous edge-cases for full-fledged objects, such as

class A
{
    friend bool operator<(A const &, A const &)
    {
        return false;
    }
};

int main()
{
    const A & a_ = ::max(A(), A());
    A a = a_; // Is a_ a dangling reference?
}

Am I correct that this usage of max - in which rvalues defined within the call argument list are passed as arguments - is an example of a potential "gotcha" with the use of max, unavoidable since max needs to be defined to return a reference in order to avoid expensive copies (and to avoid the requirement of a copy constructor)?

Might there be real-life circumstances in which it would be good programming practice to define a version of max that returns its result by value, rather than by reference, for the reason discussed in this question?

Dan Nissenbaum
  • 13,558
  • 21
  • 105
  • 181

1 Answers1

17

Yes, it's a dangling reference.

Yes, it's a gotcha.

A version that returns by value might be useful provided that you use it by default, and switch to by-reference in the cases where you need it. If you use the by-reference one by default and only switch to the by-value when you need it then you'll still fall foul of the gotcha, because realizing that you need it is the same thing as realizing you should have written const A a_= ::max(A(), A());.

Unfortunately the by-value one introduces a new gotcha:

A a, b, c;
mymax(a, b) = c; // assigns to temporary, not to a or b

(Actually, looking at this code I reckon if you call it max_by_val then you won't write this).

You could return by const value, and some people used to recommend that operator overloads should return by const value. But that introduces a performance gotcha in C++11:

A a, b, c;
c = constmymax(a, b); // copies from const rvalue instead of moving.

and even in C++03 it prevents the equivalent explicit optimization swap(c, constmymax(a,b)).

Steve Jessop
  • 273,490
  • 39
  • 460
  • 699
  • +1 FYI, I *think* C++11 8.5.3-p5 and C++11 12.2 cover the lifetime of temporaries bound to references, etc. – WhozCraig Dec 05 '12 at 11:16
  • Obviously you have the weight of rep behind you, but I'm interested in why both answers are up voted. Is it dangling or Not? – Caribou Dec 05 '12 at 11:24
  • I upvoted Arne's answer for the useful link to the GOTW article related to the subject. – Dan Nissenbaum Dec 05 '12 at 11:25
  • @Dan That’s commendable, but his answer is factually *wrong* (and in a quite subtle way that is bound to confuse people) and upvoting the answer gives the impression that it’s correct, even if the comments say otherwise. I’d therefore ask you to please not upvote such answers in the future. – Konrad Rudolph Dec 05 '12 at 11:27
  • 7
    @Caribou: don't trust rep, for all you know I'm a confident blagger and Arne's a C++ guru who happens not to be very active on the site. IIRC the wording in the standard is subtly misleading, it does just say "if a reference is bound to a temporary then...". It doesn't spell out that initializing a reference via another reference isn't "binding to the temporary" for that purpose. – Steve Jessop Dec 05 '12 at 11:30
  • @SteveJessop thanks - I think that 119k is a fair weight and no offence to the aother answerer but I had seen your replies before so asked you - Thanks for clearing it up. – Caribou Dec 05 '12 at 11:34
  • 2
    all: edited and removed my wrong answer below. Thanks for pointing that out and giving me the opportunity to learn something new. @SteveJessop: Yes, have not been very active in the past, in fact, it's been only two weeks since i registered my account. But I would not call me a C++ guru - thank you for the compliment anyways :-) – Arne Mertz Dec 05 '12 at 12:08