Samaursa
Samaursa

Reputation: 17197

Return a reference but prevent it from being stored through a wrapper or other means

In our code we sometimes return a non-const reference to the object for chaining method calls. For example:

registry.GetComponent(entityID).GetParams().GetTuners().SetWidth(50)

Sometimes in our reviews, we receive code that stores the reference and does something with it, which we forbid.

auto& comp = registry.GetComponent(2);

// ... do something with component
// ... with our pooling, comp could invalid at any point

We catch it most of the time, but sometimes it goes through and results in hard to find crashes. We tried implementing a wrapper (which stores private reference internally) that is returned instead of the references and this wrapper is non-copyable. Since the dot operator cannot be overloaded (that would have been perfect in this case), we overloaded the () operator instead.

So now we have

// all GetXXXXX are now const ref
// registry.GetComponent(entityID).GetParams().GetTuners().SetWidth(50)
registry.UpdateComponent(entityID)().UpdateParams()().UpdateTuners()().SetWidth(50)

// this is still possible, but easier to catch in code reviews
auto comp = registry.UpdateComponent(entityID);
comp().UpdateParams()().UpdateTuners()().SetWidth(50);

Ugly, but much more visible in code reviews, and if stored locally and then used, it's very easy to spot in code reviews as well.

Is it possible to return a reference to the modifiable object but prevent it from being stored OR a different pattern that allows us to safely modify nested objects.

Notes: The above reflect our code somewhat, but are very contrived examples. I hope the intent of the question is clear.

Upvotes: 2

Views: 135

Answers (1)

Quimby
Quimby

Reputation: 19123

Full disclosure, I did not think it through too much.

If you want to ensure that the reference can only be used to chaining, return by && and implement the methods only for r-value this.

#include <utility>

struct Component{
    Component&& incValue() &&{return std::move(*this);}
    Component&& decValue() &&{return std::move(*this);}
    const Component&& print() const &&{return std::move(*this);} 
// Make copy,move ctors private to disallow copies if necessary.
};

struct Registry{
    Component test;
    const Component&& getConstComponent(){ return std::move(test); }
    Component&& getComponent(){ return std::move(test); }
};
int main()
{
    Registry registry;

    registry.getComponent().incValue().decValue().print();
    registry.getConstComponent().print();

    //ERROR, & cannot bind &&
    //Component& comp = registry.getComponent();
    //const & can bind &&
    const Component& comp = registry.getComponent();
    // ERROR, print only works with const &&
    // comp.print();
    // Yes, this works (and is perfectly valid code), it is not fool-proof. 
    std::move(comp).print();
    //BEWARE, this allows easy theft of the Component's data via move ctor.
    Component thief{registry.getComponent()};
    return 0;
}

I do not recommend doing this, I only say it is possible and maybe it could be modified to help you. I would argue that not many developers are familiar with overloading based &,&& and it can become confusing.

Furthermore, it abuses move semantic by not doing any transfers of resources. Also, it can be overridden with std::move or direct cast, but it should at least give the user a pause.

This might not work if you use Component somewhere in normal way. One not so good workaround is to make l-value methods private and use friend. Much more sane version is to apply this logic to the wrapper only. The downside is the duplication of Component's interface.

  • Registry::getComponent() returns by value a CompRef wrapper that only has r-value methods which also return by value a copy of itself. This wrapper should be small, so hopefully return CompRef by value+optimization == returning Component&.
  • Consider making copy, move ctors private to disallow storage. Since every copy is made inside CompRef itself.

Note: returning r-value of itself will only work in the chain, storing it will create a dangling reference since the original was a temporary. This should not be an issue since you should not really use it, but any access will become UB.

A middleground approach, but quite invasive, would to be partition Component into l-value and r-value(=chain-only) methods via inheritance. Then, Registry could return a r-value reference to the r-value view of the component only. This would allow to use the component both in normal and chain-only context.

Again, this will require more thought before putting in any codebase.

Upvotes: 1

Related Questions