Dear Readers!

Today I stumbled upon a very peculiar feature of C++ while browsing through Qt’s image class, namely QImage. This class has two member function declarations that caught my attention:

C++

1
2
QImage convertToFormat(QImage::Format format, Qt::ImageConversionFlags flags = Qt::AutoColor) const &;
QImage convertToFormat(QImage::Format format, Qt::ImageConversionFlags flags = Qt::AutoColor) &&;

As you can see, both of these functions have ref-qualifiers at the very end of the declarations, which I found to be rather unusual based on what I have seen before (which is admittedly not that much so far). So what are these and what do they do?

Let’s first discuss what they are not:
The ref qualifiers at the end

  • will not change the type of the return value, and has nothing to do with them.
  • will not change the type of *this, as according to the standard, it is always an lvalue.

Now that we know what these ref-qualifiers will not do, let’s explore what they will do.
Apparently, as with many things in C++, these help with overload resolution. This was already hinted by the fact, that the second function above is just an overload of the first one, but for those who encountered such a thing the first time, this fact alone might not be enough to dispel all the mist around this feature. Thus, let’s see a short simple example how these qualifiers affect function call resolution.

C++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream> 

struct MyStruct
{
    void funct() &
    {
        std::cout << "MyStruct was lvalue this time." << std::endl;
    }
    
    void funct() &&
    {
        std::cout << "MyStruct was rvalue this time." << std::endl;
    }
};

int main()
{
    MyStruct ms;
    
    ms.funct();
    MyStruct().funct();
}

As one can see, the first call to funct() was through a named object, and the second through a temporary one, and as such the end result is really what one would expect:

Shell

1
2
MyStruct was lvalue this time.
MyStruct was rvalue this time.

All this means, that by simply providing ref-qualifiers at the end of the function overloads, we can tell the compiler which function to call based on the r/lvalueness of the particular object the functions were called from, which obviously opens up possibilities for further optimizations. I think everybody can agree that this is quite a neat feature, so let’s see some other interesting examples to understand it a bit better:

C++

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <iostream> 

struct MyStruct
{
    void funct() &
    {
        std::cout << "MyStruct was lvalue." << std::endl;
    }
    
    void funct(int arg) &
    {
        std::cout << "MyStruct was lvalue and had a by-value argument " << arg << std::endl;
    }
        
    void funct() const &
    {
        std::cout << "MyStruct was lvalue and const." << std::endl;
    }

    void funct(const int& arg) const &
    {
        std::cout << "MyStruct was lvalue, const and had a const lvalue reference argument " << arg << std::endl;
    }
    
    void funct(int&& arg) const &
    {
        std::cout << "MyStruct was lvalue, const and had an rvalue reference argument " << arg << std::endl;
    }
    
    void funct() &&
    {
        std::cout << "MyStruct was rvalue." << std::endl;
    }
    
    void funct(const int& arg) &&
    {
        std::cout << "MyStruct was rvalue and had a const lvalue reference argument " << arg  << std::endl;
    }
};

int main()
{
    int myNum = 5;
    
    MyStruct ms;
    
    const MyStruct& msRef = ms;
    
    ms.funct();
    ms.funct(3);
    msRef.funct();
    msRef.funct(myNum);
    msRef.funct(3);
    MyStruct().funct();
    MyStruct().funct(myNum);
}

As the example shows, there is nothing to prevent the user to combine these sort of ref-qualifiers with other (usually placed) ref-qualifiers and cv-qualifiers. Moreover, if one so desires, a function overload can be provided for just about any life situation. Here are the results:

Shell

1
2
3
4
5
6
7
MyStruct was lvalue.
MyStruct was lvalue and had a by-value argument 3
MyStruct was lvalue and const.
MyStruct was lvalue, const and had a const lvalue reference argument 5
MyStruct was lvalue, const and had an rvalue reference argument 3
MyStruct was rvalue.
MyStruct was rvalue and had a const lvalue reference argument 5

And this did not even cover all possibilities, but I think it already made the point. One can truly have a function overload for whatever state the object or the provided objects were, which yet again demonstrates how flexible C++ really is. Of course, just as with many other features, there are some limitations one needs to keep in mind if one decides to use ref-qualifiers in such a way: they cannot be placed randomly to any overload. Either all of the overloads have ref-qualifiers, or none of them. The code below will not compile:

C++

1
2
3
4
5
6
struct MyStruct {
    
    void funct();
    void funct() &&;

};

And here’s the compiler error message:

Shell

1
2
3
4
5
6
error: ‘void MyStruct::funct() &&’ cannot be overloaded with ‘void MyStruct::funct()’
     void funct() &&
          ^~~~~
note: previous declaration ‘void MyStruct::funct()’
     void funct() {
          ^~~~~

To sum it all up, all I can say that despite this is a very neat feature (putting ref-qualifiers at the end of function declarations), it is not really advertised in too many places, especially in books specifically dealing with C++, like Effective Modern C++ by S. Meyers. To be honest, I was a bit surprised by this, but hey, who am I to tell what should be included in these books. Nevertheless, interested readers can read more on the subject in the following post . That’s it for today, I hope you enjoyed it.

Thanks for reading.