Damon Damon - 16 days ago 5
C++ Question

Hint compiler to return a reference making 'auto' behave

(possibly related to How to implement a C++ method that creates a new object, and returns a reference to it which is about something different, but incidentially contains almost exactly the same code)

I would like to return a reference to a static local from a static function. I can get it to work, of course, but it's less pretty than I'd like.

Can this be improved?

The background



I have a couple of classes which don't do much except acquire or initialize a resource in a well-defined manner and reliably, and release it. They don't even need to know an awful lot about the resource themselves, but the user might still want to query some info in some way.

That's of course trivially done:

struct foo { foo() { /* acquire */ } ~foo(){ /* release */ } };

int main()
{
foo foo_object;
// do stuff
}


Trivial. Alternatively, this would work fine as well:

#include <scopeguard.h>
int main
{
auto g = make_guard([](){ /* blah */}, [](){ /* un-blah */ });
}


Except now, querying stuff is a bit harder, and it's less pretty than I like. If you prefer Stroustrup rather than Alexandrescu, you can include GSL instead and use some concoction involving
final_act
. Whatever.

Ideally, I would like to write something like:

int main()
{
auto blah = foo::init();
}


Where you get back a reference to an object which you can query if you wish to do that. Or ignore it, or whatever. My immediate thought was: Easy, that's just Meyer's Singleton in disguise. Thus:

struct foo
{
//...
static bar& init() { static bar b; return b; }
};


That's it! Dead simple, and perfect. The
foo
is created when you call
init
, you get back a
bar
that you can query for stats, and it's a reference so you are not the owner, and the
foo
automatically cleans up at the end.

Except...

The issue



Of course it couldn't be so easy, and anyone who has ever used range-based for with
auto
knows that you have to write
auto&
if you don't want surprise copies. But alas,
auto
alone looked so perfectly innocent that I didn't think of it. Also, I'm explicitly returning a reference, so what can
auto
possibly capture but a reference!

Result: A copy is made (from what? presumably from the returned reference?) which of course has a scoped lifetime. Default copy constructor is invoked (harmless, does nothing), eventually the copy goes out of scope, and contexts are released mid-operation, stuff stops working. At program end, the destructor is called again. Kaboooom. Huh, how did that happen.

The obvious (well, not so obvious in the first second!) solution is to write:

auto& blah = foo::init();


This works, and works fine. Problem solved, except... except it's not pretty and people might accidentially just do it wrong like I did. Can't we do without needing an extra ampersand?

It would probably also work to return a
shared_ptr
, but that would involve needless dynamic memory allocation and what's worse, it would be "wrong" in my perception. You don't share ownership, you are merely allowed to look at something that someone else owns. A raw pointer? Correct for semantics, but... ugh.

By deleting the copy constructor, I can prevent innocent users from running into the forgot-& trap (this will then cause a compiler error).

That is however still less pretty than I would like. There must be a way of communicating "This return value is to be taken as reference" to the compiler? Something like
return std::as_reference(b);
?

I had thought about some con trick involving "moving" the object without really moving it, but not only will the compiler almost certainly not let you move a static local at all, but if you manage to do it, you have either changed ownership, or with a "fake move" move-constructor again call the destructor twice. So that's no solution.

Is there a better, prettier way, or do I just have to live with writing
auto&
?

Answer

Something like return std::as_reference(b);?

You mean like std::ref? This returns a std::reference_wrapper<T> of the value you provide.

static std::reference_wrapper<bar> init() { static bar b; return std::ref(b); }

Of course, auto will deduce the returned type to reference_wrapper<T> rather than T&. And while reference_wrapper<T> has an implicit operatorT&, that doesn't mean the user can use it exactly like a reference. To access members, they have to use -> or .get().

That all being said however, I believe your thinking is wrong-headed. The reason is that auto and auto& are something that every C++ programmer needs to learn how to deal with. People aren't going to make their iterator types return reference_wrappers instead of T&. People don't generally use reference_wrapper in that way at all.

So even if you wrap all of your interfaces like this, the user still has to eventually know when to use auto&. So really, the user hasn't gained any safety, outside of your particular APIs.

Comments