VC++ 2014 is finally supporting ref-qualifiers, maybe a lesser know feature in C++11 that goes hand in hand with rvalue references. In this post I will explain what ref-qualifiers are and why they are important.

But before talking about ref-qualifiers let’s talk about the well known cv-qualifiers.

cv-qualifiers

Let’s take the following example of a type foo that has two overloaded methods, one const and one not const .

struct foo { void test() { std::cout << "test" << std::endl; } void test() const { std::cout << "test const" << std::endl; } }; 1 2 3 4 5 struct foo { void test ( ) { std :: cout << "test" << std :: endl ; } void test ( ) const { std :: cout << "test const" << std :: endl ; } } ;

The following code prints either “test” or “test const” depending on whether the object function test() is called on is const or not.

foo f1; f1.test(); // prints "test" foo const f2; f2.test(); // prints "test const" foo().test(); // prints "test" 1 2 3 4 5 6 7 foo f1 ; f1 . test ( ) ; // prints "test" foo const f2 ; f2 . test ( ) ; // prints "test const" foo ( ) . test ( ) ; // prints "test"

Notice that the const / volatile specification is not a constrain on the function, but on the implied this parameter. A const function can freely modify state or call non-const methods, but not state or non-const methods that belong to the object referred by this .

Let’s consider a second example where he have a Widget contained within a bar . The bar has methods to return the internal state. If the object is const , the overload resolution picks the const method, if it is non-const it picks the non-const method.

struct Widget { Widget(std::string const & name) : name(name) {}; Widget(Widget const & w) { std::cout << "copy" << std::endl; name = w.name; } Widget(Widget&& w) { std::cout << "move" << std::endl; name = std::move(w.name); } private: std::string name; }; struct bar { Widget& data() { return _widget; } Widget const& data() const { return _widget; } private: Widget _widget{ "bar" }; }; bar b1; auto w1 = b1.data(); // prints copy bar const b2; auto w2 = b2.data(); // prints copy auto w3 = bar().data(); // prints copy 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 struct Widget { Widget ( std :: string const & name ) : name ( name ) { } ; Widget ( Widget const & w ) { std :: cout << "copy" << std :: endl ; name = w . name ; } Widget ( Widget && w ) { std :: cout << "move" << std :: endl ; name = std :: move ( w . name ) ; } private : std :: string name ; } ; struct bar { Widget & data ( ) { return _widget ; } Widget const & data ( ) const { return _widget ; } private : Widget _widget { "bar" } ; } ; bar b1 ; auto w1 = b1 . data ( ) ; // prints copy bar const b2 ; auto w2 = b2 . data ( ) ; // prints copy auto w3 = bar ( ) . data ( ) ; // prints copy

The problem with this code is that in all cases the Widget was copied even though in the last example the Widget owner was an rvalue reference and the object could have been moved.

To fix this problem we can add a new method that returns an rvalue reference. However, the problem now is that we cannot have two overloaded methods, one that returns a lvalue reference and one that returns an rvalue reference. So the best we could do is this:

struct bar { Widget const & data() const { return _widget; } Widget&& data() { return std::move(_widget); } private: Widget _widget{ "bar" }; }; bar b1; auto w1 = b1.data(); // prints move bar const b2; auto w2 = b2.data(); // prints copy auto w3 = bar().data(); // prints move 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 struct bar { Widget const & data ( ) const { return _widget ; } Widget && data ( ) { return std :: move ( _widget ) ; } private : Widget _widget { "bar" } ; } ; bar b1 ; auto w1 = b1 . data ( ) ; // prints move bar const b2 ; auto w2 = b2 . data ( ) ; // prints copy auto w3 = bar ( ) . data ( ) ; // prints move

This fixed the 3rd case with the rvalue reference, but it broke the first object. After calling b1.data() the Widget from b1 was moved to w1 .

What’s the solution?

Enter ref-qualifiers

ref-qualifiers are a way to help the compiler decide which function to call when the object is an lvalue or an rvalue. Since the object is passed as an implicit pointer type parameter to the function (pointer this ) the ref-qualifiers have been also referred as “rvalue reference for *this”.

ref-qualifiers are specified with & for lvalues and && for rvalues at the end of the function signature after the cv-qualifiers.

struct bar { Widget& data() & { return _widget; } Widget const & data() const & { return _widget; } Widget&& data() && { return std::move(_widget); } private: Widget _widget{ "bar" }; }; 1 2 3 4 5 6 7 8 9 struct bar { Widget & data ( ) & { return _widget ; } Widget const & data ( ) const & { return _widget ; } Widget && data ( ) && { return std :: move ( _widget ) ; } private : Widget _widget { "bar" } ; } ;

The following code now prints “copy”, “copy”, “move” as expected.

bar b1; auto w1 = b1.data(); // prints copy bar const b2; auto w2 = b2.data(); // prints copy auto w3 = bar().data(); // prints move 1 2 3 4 5 6 7 bar b1 ; auto w1 = b1 . data ( ) ; // prints copy bar const b2 ; auto w2 = b2 . data ( ) ; // prints copy auto w3 = bar ( ) . data ( ) ; // prints move

One important thing to note is that you cannot mix ref-qualifier and non-ref-qualifier overloads. You must decided over one or another set of overloads. The following is illegal:

Widget& data() { return _widget; } // no ref-qualifier Widget const & data() const { return _widget; } // no ref-qualifier Widget&& data() && { return std::move(_widget); } // ref-qualifier 1 2 3 Widget & data ( ) { return _widget ; } // no ref-qualifier Widget const & data ( ) const { return _widget ; } // no ref-qualifier Widget && data ( ) && { return std :: move ( _widget ) ; } // ref-qualifier

The ref-qualifiers help avoiding unnecessary calls/operations on rvalue references which is helpful when may involve large objects. But they are also helpful to avoid making coding mistakes. Here is an example. Consider the following type:

struct foo { foo(int const value = 0) :value(value) {} foo& operator=(foo const & other) { this->value = other.value; return *this; } operator int() const { return value; } private: int value; }; foo get_foo() { return foo(13); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 struct foo { foo ( int const value = 0 ) : value ( value ) { } foo & operator = ( foo const & other ) { this -> value = other . value ; return * this ; } operator int ( ) const { return value ; } private : int value ; } ; foo get_foo ( ) { return foo ( 13 ) ; }

You can write things like this:

foo() = 42; if (get_foo() = 42) { } 1 2 3 4 5 foo ( ) = 42 ; if ( get_foo ( ) = 42 ) { }

Probably the first example is a little bit silly, you don’t do that kind of mistake in real life, but it’s still legal code that executes, and is not right because there’s an rvalue reference on the left side of the assignment operator. The second example is definitely a much realistic example. Sometimes we just type = instead of == in conditional expressions and what the code will do is assigning 42 to temporary, instead of testing their equality.

If we changed the signature of foo’s operator= to include a ref-qualifier (as shown below) the compiler would flag immediately both examples above as errors:

foo& operator=(foo const & other) & { // ... } 1 2 3 4 foo & operator = ( foo const & other ) & { // ... }

VC++ 2014 now flags the following error:

error C2678: binary ‘=’: no operator found which takes a left-hand operand of type ‘foo’ (or there is no acceptable conversion)

Compiler support

See also

Share this: Twitter

LinkedIn

StumbleUpon

Facebook

Reddit

More

Google

Email



Print

