7

The following code causes undefined behaviour:

class T
{
public:
    const std::string& get() const { return s_; }

private:
    std::string s_ { "test" };
}

void breaking()
{
    const auto& str = T{}.get();
    // do sth with "str" <-- UB
}

(because lifetime extension by const& doesn't apply here, as it's my understanding).

To prevent this, one solution might be to add a reference qualifier to get() to prevent it being called on LValues:

const std::string& get() const & { return s_; }

However, because the function now is both const and & qualified, it is still possible to call get() on RValues, because they can be assigned to const&:

const auto& t = T{};        // OK
const auto& s1 = t.get();   // OK
const auto& s2 = T{}.get(); // OK <-- BAD

The only way to prevent this (as far as I can see) is to either overload get() with a &&-qualified variant that doesn't return a reference, or to = delete it:

const std::string& get() const &  { return s_; }

const std::string& get() const && = delete;       // Var. 1
      std::string  get() const && { return s_; }; // Var. 2

However, this implies that to implement getter-functions that return (const) references correctly, I always have to provide either Var. 1 oder 2., which amounts to a lot of boilerplate code.

So my question is: Is there a better/leaner way to implement getter-funtions that return references, so that the compiler can identify/prevent the mentioned UB-case? Or is there a fundamental flaw in my understanding of the problem?

Also, so far I couldn't find an example where adding & to a const member function brings any advantages without also handling the && overload...maybe anyone can provide one, if it exists?

(I'm on MSVC 2019 v142 using C++17, if that makes any difference)

Thank you and best regards

Tim
  • 143
  • 1
  • 5
  • Well, one thing is extending a lifetime of a `prvalue` object. But you are trying to extend a lifetime of an object's member. How do you expect it to work? It would be very confusing for another user to see such code. I would just stick with `const auto& t = T{};` and using a reference to its member. That way the intent is clear. Otherwise I don't see any guarantee how a compiler would extend a lifetime of a member while destroying its owner object. – Sergey Kolesnik Aug 23 '21 at 12:02
  • Unfortunately, C++ doesn't have lifetime in its type system (contrary to Rust). – Jarod42 Aug 23 '21 at 12:05
  • @SergeyKolesnik: lifetime extension does apply to object member: `const auto& s = T{}.s_;` is valid (ignoring the `private:`). (Another reason why getter are bad? ;-) ) – Jarod42 Aug 23 '21 at 12:09
  • I think the [COW](https://en.wikipedia.org/wiki/Copy-on-write) pattern here could be useful instead of ref-qualifier return value. – Ghasem Ramezani Aug 23 '21 at 12:21
  • caller just has to know that binding reference to function called on prvalue can have trouble – M.M Aug 23 '21 at 12:22
  • @SergeyKolesnik That's what I was trying to say: that "T{}.get();" should not be used, and I was looking for an elegant way to enforce this in the interface of my class. I don't want lifetime extension on members of a temp object, I want an interface which prohibits that use. Sorry if my intent wasn't clear. – Tim Aug 23 '21 at 12:50
  • @Tim I suggest you rename your question just like that. But I don't thin it would be a problem of an interface. It's the use of a language in general... If it accepts dangling references, you can do nothing to prevent their usage – Sergey Kolesnik Aug 23 '21 at 12:57

1 Answers1

0

It's somewhat unclear what limitations you're working with. If it is an option, you could get rid of the getter(s), and let lifetime extension do its thing:

struct T
{
    std::string s_ { "test" };
};

const auto& str = T{}.s_; // OK; lifetime extended

With getters, you have the options of 1. providing duplicate getters or 2. accept that the caller must be careful to not assume that a reference from getter of a temporary would remain valid. As shown in the question.


You could keep private access while still making lifetime management easy by using shared ownership:

class T
{
    std::shared_ptr<std::string> s = std::make_shared<std::string>("test");
public:
    // alternatively std::weak_ptr
    const std::shared_ptr<const std::string>
    get() const {
        return s;
    }
};

But you must consider whether the runtime cost is worth the easiness.

eerorika
  • 232,697
  • 12
  • 197
  • 326