c++ref-qualifier

Generating the necessary ref-qualified overloads for a member function


I have this class:

template<typename T, size_t N>
class Array {
    private:
        T array[N];

    public:
        template <typename... InitValues>
        constexpr Array(InitValues... init_values) 
            : array{ init_values... } {}

        [[nodiscard]]
        consteval int len() const noexcept { return sizeof(array) / sizeof(T); }
}

I would like to know, for such a simple member function, when I should provide the necessary ref-qualified overloads.

With the actual code, I can compile and run the following code:

constexpr collections::Array a = collections::Array<long, 5>{1L, 2L, 3L};
SECTION("length of the array") {
    REQUIRE( a.len() == 5 );
    REQUIRE( collections::Array<int, 1>{1}.len() == 1 );
}

1- Why I can compile the second REQUIRE that contains the call with the rvalue?

Now I am gonna change the len() member function to this:

[[nodiscard]]
consteval int len() const& noexcept { return sizeof(array) / sizeof(T); }

2- Why I can compile both with the const&? I suppose that they are two are different ref-qualified usages. I assume that I can make the call with the first one, which is an lvalue, but can't understand why I can compile the second having defined the len() method as const&.

Last change:

[[nodiscard]]
consteval int len() const&& noexcept { return sizeof(array) / sizeof(T); }

And finally, I got a compiler error on a.get<I>().

'this' argument to member function 'len' is an lvalue, but function has rvalue ref-qualifier
        REQUIRE( a.len() == 5 );

that works perfect if I comment that line of code and I just run:

REQUIRE( collections::Array<int, 1>{1}.len() == 1 );

and also I could use std::move(a) to perform the cast of a to an rvalue reference and make the code compile. But I don't want to do that.

EDIT:

I will add another member function that could potentially do different things based on the ref-qualified implementation (or that what I am suppose that could happen):

template <size_t I>
requires concepts::AccessInBounds<I, N>
constexpr T get() const noexcept {
    return array[I];
}

template <size_t I>
requires concepts::AccessInBounds<I, N>
constexpr T& get() const& noexcept {
    return array[I];
}

Solution

  • To question 1: why not? The rule is the same as for lvalues: you can call const member functions regardless of the constness of the object.

    To question 2: Because it is meant to be identical to having a const& function parameter: the function can be called with any lvalue or rvalue. It exists primarily to allow you to distinguish between lvalue and rvalue overloads:

    class Array {
        // These two declarations would be ambiguous for Array rvalues
        // int len() const;
        // int len() &&;
    
        // These are not: your test expressions will use different overloads 
        int len() const&;
        int len() &&;
    };
    

    The two functions in your edit are also ambiguous, for both lvalues and rvalues. A motivating example would be more along these lines: suppose my class provides functionality to some resource that could be expensive to copy, but is cheaper to move, say a std::vector.

    template<class T>
    class VectorView {
        std::vector<T> vector;
    
    public:
        // ...
    
        constexpr std::vector<T> const& base() const noexcept { return vector; }
    };
    

    Now there is no way for a user of this class to transfer ownership of the vector data back from a view object, even if that would be useful when calling the base() function on an rvalue. Because it is in the spirit of C++ to avoid paying for things you do not need, you could allow this by adding an rvalue-qualified overload that instead returns an rvalue reference using std::move.

    So the answer to whether you need this kind of overload is it depends, which is unfortunately also in the spirit of C++. If you were implementing something like my example class for the standard library, then you certainly would, because it is based on std::ranges::owning_view. As you can see on that page, it covers all four possible base()s. If you were instead only using a reference to a source range, it would be unexpected and inappropriate to move from that object, so the related ref_view only has a const base() function like the one I wrote.

    Edit As for move semantics, the difference between something like an array and a vector is that Array<T,N> is based on T[N], while std::vector<T> is based on T*. Moving the array requires N move operations (linear time complexity), and whether a move is an improvement over a copy depends on T. Also, it needs memory space for 2N elements. On the other hand, a vector only ever needs three pointers to do its job, so it can be moved in constant time, while copying still takes linear time.

    This potential gain is the rationale for move semantics and rvalue references in a nutshell. The ability to also have &&-qualified member functions completes this language feature, but is not as significant as move constructors and assignment functions. I also found the answers to this question useful, as they give some more examples of ref-qualified overloads.