8

(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&?

Community
  • 1
  • 1
Damon
  • 67,688
  • 20
  • 135
  • 185
  • 3
    Scott meyers did a lot of talks concerning auto type deduction + also it's written in his effective modern c++ book. Maybe you should check those talks (they can be found on youtube etc.) and this clarifies a bit to the "why". – Hayt Sep 06 '16 at 13:27
  • 1
    [This talk specifically](https://www.youtube.com/watch?v=wQxj20X-tIU), I agree with @Hayt that it's well worth watching. – Borgleader Sep 06 '16 at 13:33
  • "*There must be a way of communicating "This return value is to be taken as reference" to the compiler?*" Why do people always think "there *must* be a way to do this thing I want done." No, there doesn't *have* to be. – Nicol Bolas Sep 06 '16 at 13:33
  • I the case of the shared_ptr ("you are merely allowed to look at something that someone else owns"), you could use a weak_ptr. – Janosimas Sep 06 '16 at 14:19
  • I don't understand why you want to create a singleton, as destruction doesn't happen at end of scope as other snippet. – Jarod42 Sep 06 '16 at 14:21
  • 1
    `decltype(auto) blah = foo::init();` ;-) – Jesper Juhl Sep 06 '16 at 16:05

5 Answers5

5

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.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • 2
    +1 because of this - _The reason is that auto and auto& are something that every C++ programmer needs to learn how to deal with_. So true. – skypjack Sep 06 '16 at 13:50
  • Hmm... I would agree that `auto` is probably something you can expect from everybody. But it's easy to make the silly mistake, I forgot it myself, q.e.d.. So would be nice if there was just a way to prevent yourself and others from being stupid, because... it happens. :-) Tried `std::ref` by the way, this is none better, still creates a copy (and thus fails to compile with copy ctor deleted). I'm not sure why it creates a copy, but it surely does (and causes 2 invocations of the destructor). – Damon Sep 06 '16 at 14:06
  • @Damon: Did you actually change the *return type* of the function to `reference_wrapper`? Remember: the copy happens at the *receiving* end, not the sending end. – Nicol Bolas Sep 06 '16 at 14:07
  • Aha! That did it. This indeed works without copying. Now calling `get()` is even uglier than before, but at least it's 100% foolproof :-) – Damon Sep 06 '16 at 14:15
  • 1
    I am not disputing the answer in the least. It was my first thought too. But do we really want API's that return `std::reference_wrapper` ? It's a utility wrapper to allow implementations to carry references, it's not a natural interface class... at least IMHO. – Richard Hodges Sep 06 '16 at 14:18
  • @RichardHodges: Which is why I said, "I believe your thinking is wrong-headed". – Nicol Bolas Sep 06 '16 at 14:37
5

Forcing the user to capture by reference is a three-step process.

First, make the returned thing non-copyable:

struct bar {
  bar() = default;
  bar(bar const&) = delete;
  bar& operator=(bar const&) = delete;
};

then create a little passthrough function that delivers references reliably:

namespace notstd
{
  template<class T>
  decltype(auto) as_reference(T& t) { return t; }
}

Then write your static init() function, returning decltype(auto):

static decltype(auto) init() 
{ 
    static bar b; 
    return notstd::as_reference(b); 
}

Full demo:

namespace notstd
{
  template<class T>
  decltype(auto) as_reference(T& t) { return t; }
}

struct bar {
  bar() = default;
  bar(bar const&) = delete;
  bar& operator=(bar const&) = delete;
};

struct foo
{
    //...
    static decltype(auto) init() 
    { 
        static bar b; 
        return notstd::as_reference(b); 
    }
};


int main()
{
  auto& b = foo::init();

// won't compile == safe
//  auto b2 = foo::init();
}

Skypjack noted correctly that init() could be written just as correctly without notstd::as_reference():

static decltype(auto) init() 
{ 
    static bar b; 
    return (b); 
}

The parentheses around the return (b) force the compiler to return a reference.

My problem with this approach is that c++ developers are often surprised to learn this, so it could be easily missed by a less experienced code maintainer.

My feeling is that return notstd::as_reference(b); explicitly expresses intent to code maintainers, much as std::move() does.

Richard Hodges
  • 68,278
  • 7
  • 90
  • 142
  • 2
    What is the improvement over only making the copy constructor deleted? That already fails to compile if you accidentially capture by value, too. – Damon Sep 06 '16 at 14:10
  • You don't need `nostd::as_reference`. Use `return (b);` within `init` instead, it works as well. – skypjack Sep 06 '16 at 14:11
  • @skypjack that's a fair point, although not all code readers will be aware that it generates a reference (sadly). I felt an explicit function would express intent. – Richard Hodges Sep 06 '16 at 14:13
  • @RichardHodges Fair enough, but a C++ developer must learn it sooner or later. :-) ... You could mention it and explain why it works. – skypjack Sep 06 '16 at 14:14
  • @Damon I am anticipating that this construct will eventually need to be templatised in the OP's code. decltype(auto) preserves the reference-type of any type (although in fairness, so would `auto&` as a return type) – Richard Hodges Sep 06 '16 at 14:15
  • `decltype(auto) as_reference(T& t) { return t; }` Wouldn't this have to be `return (t);`? – ildjarn Sep 06 '16 at 21:31
  • @ildjarn no. this is the difference between `auto` and `decltype(auto)`. The latter preserves references. – Richard Hodges Sep 06 '16 at 21:35
  • @RichardHodges : Ah, I thought return expressions that were variable names got special treatment, and VC++ was agreeing with me; I see now that it's a VC++ bug. Thanks! – ildjarn Sep 06 '16 at 21:42
  • @ildjarnn does anyone use microsoft anything anymore? ;-) – Richard Hodges Sep 06 '16 at 21:44
1

If you want to use singleton, use it correctly

class Singleton
{
public:
    static Singleton& getInstance() {
        static Singleton instance;
        return instance;
    }

    Singleton(const Singleton&) = delete;
    Singleton& operator =(const Singleton&) = delete;

private:
    Singleton() { /*acquire*/ }
    ~Singleton() { /*Release*/ }
};

So you cannot create copy

auto instance = Singleton::getInstance(); // Fail

whereras you may use instance.

auto& instance = Singleton::getInstance(); // ok.

But if you want scoped RAII instead of singleton, you may do

struct Foo {
    Foo() { std::cout << "acquire\n"; }
    ~Foo(){ std::cout << "release\n"; }

    Foo(const Foo&) = delete;
    Foo& operator =(const Foo&) = delete;

    static Foo init() { return {}; }
};

With the usage

auto&& foo = Foo::init(); // ok.

And copy is still forbidden

auto foo = Foo::init(); // Fail.
Jarod42
  • 203,559
  • 14
  • 181
  • 302
1

The best, most idiomatic, readable, unsurprising thing to do would be to =delete the copy constructor and copy assignment operator and just return a reference like everybody else.

But, seeing as you brought up smart pointers...

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.

A raw pointer would be perfectly acceptable here. If you don't like that, you have a number of options following the "pointers" train of thought.

You could use a shared_ptr without dynamic memory, with a custom deleter:

struct foo {
    static shared_ptr<bar> init() { static bar b; return { &b, []()noexcept{} }; }
}

Although the caller doesn't "share" ownership, it's not clear what ownership even means when the deleter is a no-op.

You could use a weak_ptr holding a reference to the object managed by the shared_ptr:

struct foo {
    static weak_ptr<bar> init() { static bar b; return { &b, []()noexcept{} }; }
}

But considering the shared_ptr destructor is a no-op, this isn't really any different from the previous example, and it just imposes on the user an unnecessary call to .lock().

You could use a unique_ptr without dynamic memory, with a custom deleter:

struct noop_deleter { void operator()() const noexcept {} };
template <typename T> using singleton_ptr = std::unique_ptr<T, noop_deleter>;

struct foo {
    static singleton_ptr<bar> init() { static bar b; return { &b, {} }; }
}

This has the benefit of not needing to manage a meaningless reference count, but again the semantic meaning is not a perfect fit: the caller does not assume unique ownership, whatever ownership really means.

In library fundamentals TS v2 you can use observer_ptr, which is just a raw pointer that expresses the intent to be non-owning:

struct foo {
    static auto init() { static bar b; return experimental::observer_ptr{&b}; }
}

If you don't like any of these options, you can of course define your own smart pointer type.

In a future standard you may be able to define a "smart reference" that works like reference_wrapper without the .get() by utilising overloaded operator..

Oktalist
  • 14,336
  • 3
  • 43
  • 63
0

Someone would make a typo in one day and then we may or may not notice it in so many code, although we all know we should use auto& instead of auto.

The most convenient but very dangerous solution is using a derived class, as it breaks the strict-aliasing rule.

struct foo
{
private:
    //Make these ref wrapper private so that user can't use it directly.
    template<class T>
    class Pref : public T {
    private:
        //Make ctor private so that we can issue a compiler error when 
        //someone typo an auto. 
        Pref(const Pref&) = default;
        Pref(Pref&&) = default;
        Pref& operator=(const Pref&) = default;
        Pref& operator=(Pref&&) = default;
    };
public:
    static Pref<bar>& init() { 
        static bar b; 
        return static_cast<Pref<bar>&>(b); 
    }
    ///....use Pref<bar>& as well as bar&.
};
Landersing
  • 66
  • 5