Introducing ref-qualifiers for member functions

Intro

Today we will have a look at a nice feature of C++11. It is called
ref-qualifiers for non-static member functions. This feature is there to support value semantics in C++.

Hmmh, what does that mean?

We will see ...

Motivating example

Lets say, we want to create a class that holds some precious resource, and we want to use value semantics for it. In the following example our resource holder is a CookieMonster and the precious resource it holds is a pile of Cookies. The CookieMonster also has a method Cookies getCookies() which returns the monster's cookie pile.

struct Cookies  
{
    // details unimportant
};

struct CookieMonster  
{
    Cookies _cookies;

    Cookies getCookies() const;
}

Now let's use it like this:

Cookies cookies;  
CookieMonster monster;

// returns a copy of the monster's cookie pile
cookies = monster.getCookies();

// copies the cookies out of the dying monster
cookies = CookieMonster().getCookies();  

The resource (our cookies) gets copied in both cases. But in the second example we call getCookies() on a rvalue (a temporary CookieMonster object). This object is going to die anyway on that exact same line and hence does not need its resource anymore. The copy is therefore unnecessary. It would be great if we could move it out, instead of copying it. But how do we know if getCookies() is called on a rvalue or on a lvalue? How can we make a distinction between a temporary CookieMonster and a named CookieMonster object?

This is where ref-qualifiers for non-static member functions come into play.

How it works

Ref-qualifiers allow us to overload our getCookies() method on the rvalue-ness, so to speak, of the CookieMonster object. Let's have a look at our new CookieMonster:

struct CookieMonster  
{
    Cookies _cookies;

    CookieMonster() : _cookies{} {}

    // gets called on an lvalue (a named CookieMonster)
    // read as "Cookies getCookies(CookieMonster &instance);"
    Cookies getCookies() &
    {
        cout << "[1] Here you have a copy of my cookie pile.\n";
        return _cookies;
    }

    // gets called on an lvalue (a named CookieMonster)
    // read as "Cookies getCookies(const CookieMonster &instance);"
    Cookies getCookies() const &
    {
        cout << "[2] Here you have a copy of my cookie pile.\n";
        return _cookies;
    }

    // gets called on an rvalue (a temporary CookieMonster)
    // read as "Cookies getCookies(CookieMonster &&instance);"
    Cookies getCookies() &&
    {
        cout << "[3] You're gonna steal cookies from my dead cold hands?!?\n";
        return move(_cookies);
    }

    // gets called on an rvalue (a temporary CookieMonster)
    // read as "Cookies getCookies(const CookieMonster &&instance);"
    // Quiz for the experts: in what scenario would this overload be selected?
    Cookies getCookies() const &&
    {
        cout << "[4] You're gonna steal cookies from my dead cold hands?!?\n";
        return move(_cookies);
    }
};

We have four overloads of getCookies() now. To understand when these overloads get invoked it may help to imagine them as free functions:

    Cookies getCookies(CookieMonster &);        //[1]
    Cookies getCookies(const CookieMonster &);  //[2]
    Cookies getCookies(CookieMonster &&);       //[3]
    Cookies getCookies(const CookieMonster &&); //[4]

This is a bit of an oversimplification, but we can clearly see the distinction in these overloads now.

We have overloads that gets called:

  • if the CookieMonster object is a lvalue (a named object)
  • if the CookieMonster object is a const lvalue
  • if the CookieMonster object is a rvalue (a temporary object)
  • if the CookieMonster object is a const rvalue (whatever that means)

Now we can use std::move to steal the resources if overloads [3] and [4] are selected.

Let's test our new CookieMonster implementation.

CookieMonster monster;  
auto cookies = monster.getCookies();    // [1] calls getCookies() &

const CookieMonster constMonster;  
cookies = constMonster.getCookies();    // [2] calls getCookies() const &

cookies = CookieMonster().getCookies(); // [3] calls getCookies() &&  

This outputs:

[1] Here you have a copy of my cookie pile.
[2] Here you have a copy of my cookie pile.
[3] You're gonna steal cookies from my dead cold hands?!?

We see that overload number [3] gets selected on the temporary CookieMonster and the cookies get stolen from the poor victim.

Wrapping up

We saw how ref-qualifiers for member functions help us to prevent unnecesary copies while still preserving value-semantics. One question remains for me, though. I have no idea when overload [4] would be selected or if it is even legal. The compilers i tried do not agree on this. If you have any insights please let me know!

Andre Haupt

Read more posts by this author.