Categories
C++Programming

Ref-qualified overloads – real-world example

Intro

This article is the first one of a small mini series of C++ related articles where I will show and explain some C++ features, which might be a little uncommon in its usage at least for some (or even many?) of C++ programmers – probably because the practical usage of these features is not always clear or self explained.

At least that was the case for myself – until I stumbled upon a practical use case in my own code. Exactly this learning experience (what is the (a) practical usage of that feature) I want to present in the following series of articles.

In this article I will deal with ref-qualified method overloads.

Ref-qualified overloads in C++

Since C++11 methods can have ref-qualified overloads.

class SomeClass {
public:
    // first ref-qualified overload: returning "normal" lvalue reference
    SomeClass & function() & { return *this; }

    // second ref-qualified overload: returning rvalue reference
    // a 'moved out object'
    SomeClass && function() && { return std::move(*this); }

    // there are 2 more variants for 'const' respectively,
    // which I will not cover in this article/example.
};

The first overload will just return a reference to the object, whereby the second overload will move the object out of its current class instance into a receiving object (if any). The old class instance is ’empty’ then.

As I saw such an example the first time, I was wondering what could be a real-world use case for it. Do you know any?

I discovered a nice use case example by my self as I was implementing the SourceLocation class for TeaScript.

The class SourceLoaction is a very simple data class for referencing a location inside a file or block of (script) source code. For can do this the class has only a few members: mFile to reference the name of the file where the code belongs to (if it comes from a file), mStartLine, mStartColumn, mEndLine and mEndColumn for mark the exact section of the code and finally a mSource member which represents a copy of the most relevant source code line for this section (usually a location is within one line).
The source code can be viewed on github: SourceLocation.hpp

Not every member will be used/filled in every SourceLocation instance. For example, the copy of the source code line (mSource) will only be filled, if the debug mode of the Host Application is enabled. Also, not all instances might have the mFile member set.

Furthermore, some values for a member might be known only at some later point of execution, e.g., the mEndLine and mEndColumn for an if-statement can only be set after the end of the if-statement has been parsed.

The total amount of members is (only) 6. But even though, with all that given above, how the constructor(s) should/must look like? How many combinations must be implemented and in which order should the parameters be (with or without default values)? Even with only 6 members this could be a real mess already, especially if the class interface should have a nice “user experience” for other programmers.

Instead of messing around with a huge amount of constructor and/or default value combinations, I realized that ref-qualified method overloads can help me in this concrete case.
(Side note: From the actual source code I could easily remove 2 more constructors without any pain (remaining 2 then). They just stay there because they are implemented already (they don’t harm).)

So, I added a bunch of ref-qualified methods overloads for the additional/flexible used members.
The shortest example to show is for the mFile member:

/// sets the corresponding file name.
void SetFile( std::shared_ptr<std::string> const &rFile )
{
    mFile = rFile;
}

/// adds the corresponding file name and \return a lvalue reference of this.
SourceLocation &AddFile( std::shared_ptr<std::string> const &rFile ) &
{
    SetFile( rFile );
    return *this;
}

/// adds the corresponding file name and \return a rvalue reference of a moved this.
SourceLocation && AddFile( std::shared_ptr<std::string> const &rFile ) &&
{
    SetFile( rFile );
    return std::move( *this );
}

Here is one example how I used it:
(You can find this real-world example in Exception.hpp)

// for readability I removed the surrounding code.
// assuming a class runtime_error which takes a SourceLocation 
// as first parameter in its constructor.

// 1.)
// SourceLocation created with line, column, source code line and file
//[...]
runtime_error( SourceLocation( line, col ).AddSource(std::move(lineStr)).AddFile(rFile), // [...]


// 2.)
// SourceLocation created only with a file.
//[...]
runtime_error( SourceLocation().AddFile(rFile), // [...]



As you can see with this approach I have the greatest possible flexibility. I can add (or not add) any member value in any order.

But beside that the most important thing is, that the final object is moved into the receiving function similar as if I just called one big constructor (in place construction). The anonymous (unnamed) object is moved from one call to the next in the call chain until it is moved to its final place.

What do I pay for this flexibility?

Not much!

I created a similar godbolt example: Link to godbolt.

You can switch between the 2 lines of code in the main function and then compare the assembly output of 'main:' until the call to test(SomeClass&&).
As you can see the assembly instructions of the ref-qualified method overload variant are only increased by 5. And that are mainly 'mov' instructions. A 'mov' is one of the cheapest and fastest instruction which will more or less not count at all for this example (the most time will be used in the call to puts() anyway).
Interestingly, the variant with the one big constructor adds an additional 'call' to a std::string constructor. A 'call' is a slow instruction because there is a lot more going on when calling a function as if when just moving a register value.
So, at the end it could even be, that the big constructor variant is even slower although it has slightly fewer assembly instructions. But I did not measure if that is the case.

Conclusion / Final thoughts

Personally, I think, this way of chaining an object construction, where the final object is moved to its final place at the end, is a great approach which makes the construction very flexible. The few more 'mov' assembly instructions can be paid easily without any worries for the most of the cases. Therefor the programmer gets a lot of more flexibility. So, I think, it is definitely worth it.

What do you think of this technique and/or this concrete real-world example?
I hope you enjoyed reading this article and I will ‘see’ you again in part 2 of the series. Thank you for reading. 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *