Ref-qualified member functions

C++

Today I’m going to tell you about a new and a little known (to my mind) C++ feature — reference-qualified member functions. I’ll tell about the rules of such functions overloading and, as an example of use, I’ll show you that with the help of ref-qualified you can try to improve the resource management scheme, which is implemented with the help of another C++ idiom — RAII.

Introduction

A possibility of qualifying member functions by a reference (at least it looks like a reference) has recently appeared in C++. These signs of qualification can be lvalue, rvalue references. They can also go with const qualifier.

class some_type
{
  void foo() & ; 
  void foo() && ;
  void foo() const & ;
  void foo() const && ;
};

What’s that for?

Officially this feature is called “_ref-qualifiers for *this_” or “_rvalue references for *this_”. But I think the name might confuse a bit as it may seem that the object changes the type when calling functions of different qualifications. But actually, *this type never changes. What’s the point then? Due to these qualifiers it’s possible to overload member functions in context (rvalue, lvalue, etc) the object is used in.

int main()
{
  some_type t; 
  t.foo(); // some_type::foo() & 
  some_type().foo(); // some_type::foo() && 
}

How does it Work?

To start with, for a long time C++ has had a mechanism of overload resolution between member functions and free functions. What’s it for? — you’ll ask. — We can understand whether a free function or a class method is called (at least by syntax, obj.f() in one case and just f() in another one). The thing is that when it comes to operators overloading there can be no divergences anymore. For example:

struct some_type
{
  bool operator == (int) const; 
};
bool operator == (const some_type& l, long r); 
void g()
{
  some_type t;
  int i = 42;
  t == i; // Which function should we call?
}

In order to resolve such overload the compiler presented a member function in the form of a free function with an additional parameter — a reference to an object, which had called the function. Then it resolved the overload among other free functions. So in order to implement the innovation it was necessary to “adjust” the current behavior a little bit, namely create different signatures of overload candidates for differently qualified member functions. I’ll tell you a few words about the way this mechanism operates as it’s not always obvious which function is the best candidate for overload in one case or another. Let’s consider the code from the first example one more time.

class some_type
{
  void foo() & ; // 1
  void foo() && ; // 2
  void foo() const & ; // 3
  void foo() const && ; // 4
};

void g()
{
  some_type().foo();
}

3 candidates fit this call: 2, 3 and 4. There are special rules for resolution between them. These rules look quite complex on paper, but they mean that we choose the function which corresponds mostly to the type. I’ll try to retell the line of reasoning about choosing the candidate the way I see it. In the given example the some_type() expression — rvalue. Functions 2, 3 or 4 can be potentially called. But rvalue reference qualified functions “correspond” to the initial expression (rvalue) more than const &. There are variants 2 and 4 remaining. In the fourth variant we should execute an additional action over the initial type for complete compatibility. We should add const, as there are no additional actions required in the second variant. So we’ll choose the second variant as a result.

How should I Use it?

You can use this feature when the object behavior should differ from the context it’s used in. For example, when using RAII we can make the use of pointer to the stored resource safer.

class file_wrapper
{
public:
    // ...
  operator FILE* () {return held_;}
  ~file_wrapper() {fclose(held_);}
private:
  FILE* held_;
};

In the given example operator FILE* () represents a huge hole in a safe use of file wrapper. Let’s imagine the following:

FILE* f = file_wrapper("some_file.txt", "r");
// work with f

Now we can make it handy function more secure (but not completely).

operator FILE* () & {return held_;} // We can call it for lvalue objects only

We can look at RAII from a bit different perspective. Now if we can “understand” that we are called in different contexts, let’s just pass the resource ownership instead of copying, when our object will no longer be used.

template <typename T>
class some_type
{ 
public:
  operator std::unique_ptr<T>() const &
  {
    return std::unique_ptr<T>(new T(*held_)); // Copy
  } 
  operator std::unique_ptr<T>() &&
  { 
    return std::move(held_); // Give the ownership away
  } 
private:
  std::unique_ptr<T> held_;
};

some_type f();

void g()
{
  std::unique_ptr<widget> p = f();
}

Comments

1,128

Ropes — Fast Strings

Most of us work with strings one way or another. There’s no way to avoid them — when writing code, you’re doomed to concatinate strings every day, split them into parts and access certain characters by index. We are used to the fact that strings are fixed-length arrays of characters, which leads to certain limitations when working with them. For instance, we cannot quickly concatenate two strings. To do this, we will at first need to allocate the required amount of memory, and then copy there the data from the concatenated strings.