Contents

C++ quick tips: Member functions with reference qualifier

Have you ever used member functions with reference qualifiers? This a feature introduced in C++11, being an essential part of move semantics.

Short recap

C++11 allows for specifying either lvalue or rvalue reference qualifier on a non-static member function. The reference qualifier is part of an overload resolution so, it’s possible to have the following:

 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
class Box {
public:
    void abc() & {
        std::cout << "lvalue variant" << std::endl;
    }

    void abc() && {
        std::cout << "rvalue variant" << std::endl;
    }
};

Box foo() {
    return Box{};
}

int main() {
    Box b;

    // lvalue
    b.abc();

    // rvalues
    std::move(b).abc();
    Box{}.abc();
    foo().abc();

    return 0;
}

Move semantics is not only for function arguments

Now, when would that be of any use? Imagine a data container:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Box {
public:
    const std::vector<int>& getData() const {
        return lots_of_data;
    }

private:
    std::vector<int> lots_of_data;
};

int main() {
    Box b;
    auto data = b.getData();
    return 0;
}

You often provide read only access as an optimal solution, preventing copying the vector. There’s nothing wrong with that but now, it’s possible to complement that with move semantics:

 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
class Box {
public:
    // overload #0
    const std::vector<int>& getData() const & {
        return lots_of_data;
    }

    // overload #1
    std::vector<int> getData() & {
        return lots_of_data;
    }

    // overload #2
    std::vector<int> getData() && {
        return lots_of_data;
    }

private:
    std::vector<int> lots_of_data;
};

void use(const Box& b) {
    // will use overload #0
    b.getData();
}

int main() {
    Box b;

    use(b);

    // will use overload #1
    auto copy = b.getData();

    // will use overload #2, b is now an empty husk
    auto data = std::move(b).getData();

    return 0;
}

I’ve seen this trick for the first time used in Meta’s Folly library’s Future implementation1.

In short, you use the rvalue overload to convey that the data is gonna be moved out therefore, you relinquish the ownership, transferring it via the return value to the caller.

C++23 and explicit object parameter

C++23 introduces an interesting feature which is somewhat related to member function’s reference qualifiers - explicit object parameter2. Here’s a short overview:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Box {
public:
    // This is the same as
    // const std::vector<int>& getData() const &;
    const std::vector<int>& getData(this const Box& b) {
        return b.lots_of_data;
    }

private:
    std::vector<int> lots_of_data;
};

It allows for naming this explicitly within the member functions. this is no longer an implicit variable in such function. Object data access has to happen through the object variable (which now is a reference, not a pointer).

By default named this is now a reference but it’s possible to pass the object by value (very much similar to value receivers in golang) to member functions:

1
2
3
4
5
6
    std::vector<int> getData(this Box b) {
        // b is a copy here
        // local modifications don't affect the original object
        b.lots_of_data.push_back(123);
        return b.lots_of_data;
    }

But what’s the benefit of having that? Well, you can now have a common function handling all three overloads:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14

    template <typename Self>
    auto getData(this Self&& self) {
        if constexpr (std::is_rvalue_reference_v<Self&&>) {
            std::cout << "move" << std::endl;
            return std::move(self.lots_of_data);
        } else if (std::is_same_v<Self&&, const Box&>) {
            std::cout << "constref" << std::endl;
            return self.lots_of_data;
        } else {
            std::cout << "copy" << std::endl;
            return self.lots_of_data;
        }
    }

Or even more conveniently, just drop the template and use auto:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
    auto getData(this auto&& self) {
        using Self = decltype(self);
        if constexpr (std::is_rvalue_reference_v<Self&&>) {
            std::cout << "move" << std::endl;
            return std::move(self.lots_of_data);
        } else if (std::is_same_v<Self&&, const Box&>) {
            std::cout << "constref" << std::endl;
            return self.lots_of_data;
        } else {
            std::cout << "copy" << std::endl;
            return self.lots_of_data;
        }
    }