Lifetime extension of temporaries' data members and API design
Asked Answered
E

2

8

Suppose I have a cross-platform Path class like:

class Path {
public:
    // ...
    Path parent() const;                // e.g., /foo/bar -> /foo

    std::string const& as_utf8() const {
        return path;
    }
private:
    std::string path;
};

The parent() member function returns the parent path of this path, so it (rightly) returns a newly constructed Path object that represents it.

For a platform that represents paths at the OS-level as UTF-8 strings (e.g., Unix), it seems reasonable for as_utf8() to return a reference directly to the internal representation path since it's already UTF-8.

If I have code like:

std::string const &s = my_path.as_utf8();  // OK (as long as my_path exists)
// ...
Path const &parent = my_path.parent();     // OK (temporary lifetime extended)

Both these lines are fine because:

  • Assuming my_path persists, then s remains valid.
  • The lifetime of the temporary object returned by parent() is extended by the const&.

So far, so good. However, if I have code like:

std::string const &s = my_path.parent().as_utf8(); // WRONG

then this is wrong because the temporary object returned by parent() does not have its lifetime extended because the const& does not refer to the temporary but to a data member of it. At this point, if you try to use s, you'll either get garbage or a core dump. If the code were instead:

    std::string as_utf8() const {                 // Note: object and NOT const&
        return path;
    }

then the code would be correct. However, it would be inefficient to create a temporary every time this member function is called. The implication is also that no "getter" member functions should ever return references to their data members.

If the API is left as-is, then it would seem to place an undue burden on the caller to have to look at the return type of as_utf8() to see whether it returns a const& or not: if it does, then the caller must use an object and not a const&; if it returns an object, then the caller may use a const&.

So is there any way to solve this problem such that the API is both efficient in most cases yet prevents the user from obtaining dangling references from seemingly innocuous looking code?


By the way, this was compiled using g++ 5.3. It's possible that the lifetime of the temporary should be extended, but that the compiler has a bug.

Eteocles answered 24/8, 2016 at 16:7 Comment(10)
then this is wrong because the temporary object returned by parent() does not have its lifetime extended because the const& does not refer to the temporary but to a data member of it this is wrong. I am looking for the question that shows the lifetime is extendedHaines
@NathanOliver: What's wrong about what he said? He said that the temporary's lifetime is not extended. And it won't be.Vola
@Haines [citation needed]Eteocles
@NicolBolas I believe this and its target state that a const reference to the member of a temporary is legal.Haines
Just posted a new comment since I just missed the edit deadlineHaines
@Haines But there's a difference between temp().member and temp().get() where get() happens to return member.Tufted
@Tufted I don't think so. As long as get returns a reference to the member then you have the member as the reference is just an alias.Haines
@NathanOliver: How could the compiler possibly know that get returns a reference to a member? All the compiler can see is that get returns a reference. No, temporary lifetime extension only happens when you get an NSDM, not call a member function.Vola
@Haines How will a compiler know to extend the lifetime of the temporary if get is not inline?Tufted
Okay so it is only if you take a reference to the member directly the compiler is allowed to extend the lifetime. Thanks.Haines
M
7

What you could do is create 2 different versions of as_utf8(), one when used on lvalues, and one for rvalues. You would need C++11 though.

That way, you get the best of both worlds: a const& when the object isn't a temporary, and an efficient move when it isn't:

std::string const& as_utf8() const & {
                               // ^^^ Called from lvalues only
    return path;
}

std::string as_utf8() const && {
                        // ^^^^ Called from rvalues only
    return std::move(path); //We don't need path any more
}
Mezereum answered 24/8, 2016 at 16:14 Comment(7)
What would you do if one of the platforms you had to support didn't have a C++11 compiler?Eteocles
@PaulJ.Lucas Well, it depends. I would use another platform to compile the code for the platform I had to support :) If this isn't possible, I would use the second version. Given that path isn't really long in most cases, the performance lost doesn't really matter in my opinion, as it is minimal :)Mezereum
@PaulJ.Lucas: I would stop worrying about trivial efficiency issues like this, and just return std::string by value. Actually, unless the OP has evidence that those copies are expensive, that is what I would do anyway.Aufmann
I would move path for rvalue to avoid the copy.Suisse
@Suisse Doesn't this "stop" the compiler from using RVO?Mezereum
path is not a local variable, so NRVO doesn't apply here.Suisse
@Suisse Interesting :) ThanksMezereum
P
1

To my mind the guiding principle as to whether to return a reference or an object is to examine the defined role of the originating class.

i.e. is the method exposing a simple property (argues for a reference, particularly if it's immutable), or is it generating something?

If it's generating a new object or representation we can reasonably expect it to return a distinct object.

Users of APIs are generally accustomed to understanding that properties do not outlive their host objects. This can of course be made plain in the documentation.

e.g.

struct path
{
    /// a property
    /// @note lifetime is no longer than the lifetime of this object
    std::string const& native() const;

    /// generate a new string representation in a different format
    std::string to_url() const;

};

I personally would avoid the prefix of as_ in this case since to me it suggest that we're returning a new representation of the same object, such as:

struct world 
: std::enable_shared_from_this<world>
{
    struct sky {} my_sky_;

    /// returns a shared_ptr to my sky object, which shares its lifetime
    /// with this world.
    std::shared_ptr<sky> as_sky() 
    { 
      return std::shared_ptr<sky>(shared_from_this(), std::addressof(my_sky_));
    }
};
Plasty answered 24/8, 2016 at 16:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.