3

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.

  • What is the correct way of code those examples in terms of ref-qualified overloads?
  • Don't forget about the questions on the examples above

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];
}
Alex Vergara
  • 1,766
  • 1
  • 10
  • 29
  • Will the overloads do anything diferent from the `const` version? If not, that will be sufficient. And also, here the simplest implementation is `return N;` – BoP Dec 11 '22 at 12:38
  • Not in this case, because we always are returning the size of the underlying c-style array. But for a different case, like returning a T element by ref or similar, could be the case? – Alex Vergara Dec 11 '22 at 12:41
  • @BoP I added another member function that could demonstrate better my doubt, but I am also not sure if is the case, 'cause I am lost with the `ref-qualified` implementations – Alex Vergara Dec 11 '22 at 12:47
  • 1
    I think you have a solution and are now looking for a problem that needs this solution. :-) My advice is to not be looking too hard. Except for the standard set of const and non-const overloads, this is really rare. For example, you *could* have an && qualified overload that moves the resurn value, but in reality who is a returning a whole vector and then only wants to move out one of the values?! The effort could be better spent looking into that code. So the use case is probably "hardly ever", and when it happens it is often pretty obvious. When it is not obvious, I wouldn't bother. – BoP Dec 11 '22 at 16:44
  • @BoP you're completly right! I am afraid of not having enought knowlegde and code things in a bad way for the C++ standard, so I always end with this time-consuming doubts – Alex Vergara Dec 11 '22 at 20:05

1 Answers1

1

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.

sigma
  • 2,758
  • 1
  • 14
  • 18
  • I changed my code to use std::vector instead of the Array class, because it's a clearer example of a class that can always be cheaply moved. For fixed arrays this depends on their contents. – sigma Dec 13 '22 at 15:36
  • thanks for your answer. I would like to know, why move the content of the `Array` depends on the content and move the `std::vector` doesn't have that issue? (doesn't depends on the content of the vector) – Alex Vergara Dec 13 '22 at 22:13
  • @AlexVergara My reply got too long for a comment, so I've updated my answer again! – sigma Dec 15 '22 at 23:07
  • Hope I could give you more that an "accepted answer" for your detailed, clear and wonderful response. I have to read it three times, but things are now clearer for me. Thanks. – Alex Vergara Dec 16 '22 at 08:09