Do rvalue references to const have any use?
Asked Answered
G

8

130

I guess not, but I would like to confirm. Is there any use for const Foo&&, where Foo is a class type?

Geordie answered 8/2, 2011 at 21:49 Comment(1)
In this video, STL says const&& are very important, although he doesn't say why: youtube.com/watch?v=JhgWFYfdIho#t=54m20sBowles
T
103

They are occasionally useful. The draft C++0x itself uses them in a few places, for example:

template <class T> void ref(const T&&) = delete;
template <class T> void cref(const T&&) = delete;

The above two overloads ensure that the other ref(T&) and cref(const T&) functions do not bind to rvalues (which would otherwise be possible).

Update

I've just checked the official standard N3290, which unfortunately isn't publicly available, and it has in 20.8 Function objects [function.objects]/p2:

template <class T> void ref(const T&&) = delete;
template <class T> void cref(const T&&) = delete;

Then I checked the most recent post-C++11 draft, which is publicly available, N3485, and in 20.8 Function objects [function.objects]/p2 it still says:

template <class T> void ref(const T&&) = delete;
template <class T> void cref(const T&&) = delete;
Terceira answered 9/2, 2011 at 2:8 Comment(16)
Looking at cppreference it appears this isn't the case anymore. Any ideas why? Any other places const T&& is used?Unpaid
Actually, cppreference might just not display deleted functions. Haven't checked the standard.Unpaid
@Unpaid cppreference certainly displays deleted functions, those were omitted by mistake.Exemplification
Why did you include the same code in your answer three times? I tried to find a difference for too long.Eyehole
@typ1232: It appears that I updated the answer nearly 2 years after I answered it, due to concerns in the comments that the referenced functions no longer appeared. I did a copy/paste from N3290, and from the then-lastest draft N3485 to show that the functions still appeared. Using copy/paste, in my mind at the time, was the best way to ensure that more eyes than mine could confirm that I wasn't overlooking some minor change in these signatures.Terceira
Procedure for verifying equivalence of two snippets: copy the first snippet into a text file; copy an unrelated snippet into a second text file; copy the second snippet into a third text file. Use diff to verify the first and last files are equal, and that the second is different.Advowson
Can you explain your second sentence a little bit more? The above two overloads... I don't quite follow, but it seems like an important point.Rev
@kevinarpe: The "other overloads" (not shown here, but in the standard) take lvalue references, and are not deleted. The overloads shown here are a better match for rvalues than are the overloads that take lvalue references. So rvalue arguments bind to the overloads shown here, and then cause a compile time error because these overloads are deleted.Terceira
@HowardHinnant What if the T is NOT a template parameter, but an explicitly given type such as class Bar; void Foo(Bar &&) and class Bar; void Foo(Bar const &&)? Does those two Foo make any difference?Maccarone
@DeanSeo: No difference in this context.Terceira
Why isn't T&& sufficient?Periphrasis
The use of const T&& prevents somebody foolishly using explicit template args of the form ref<const A&>(...). That's not a terribly strong argument, but the cost of const T&& over T&& is pretty minimal.Terceira
Minor comment here about this example, since I've come across this in our codebase where it is definitely wrong (because it silently turns a move into a copy). These templated functions aren't rvalue references; they are what Scott Meyers called universal references (not going to explain it here, google it). AFAICT the const in that context makes no functional difference.. But there is a difference between "void foo(Foo&& f)" and "void(const Foo&& f)" where in the second f is effectively const (this doesn't seem to happen with the universal references)..Clue
@OliverSeiler no, you're wrong. A forwarding reference is an rvalue reference to a cv-unqualified template parameter. That constraint is not the case here due to the const qualifier, i.e. they do not behave like forwarding references but are always just const rvalue references to T within the example.Bortman
@HowardHinnant - In your second line "The above two overloads..." you have mentioned, in the absence of ref(const T&&) = delete the ref(T&) can bind to rvalues. I tried to think and read around but I am not able to understand how. Is it just a recommended best practice to cover for non compliant compilers? Because, as I understand, compliant compilers will not bind an rvalue to a T&.Torrefy
Your understanding is correct. But there's a corner case you're missing. If a function returns a A const, such as A const test();, then ref(test()) would bind to ref(T&) without the protective overload because T would deduce as A const, and then ref(A const&) can bind to an rvalue. Easy case to miss since returning a const type is typically contra-indicated as it messed up move semantics. But a long time ago, before move semantics, returning const types was actually considered good style as it prevented assignment into temporaries.Terceira
M
33

The semantics of getting a const rvalue reference (and not for =delete) is for saying:

  • we do not support the operation for lvalues!
  • even though, we still copy, because we can't move the passed resource, or because there is no actual meaning for "moving" it.

The following use case could have been IMHO a good use case for rvalue reference to const, though the language decided not to take this approach (see original SO post).


The case: smart pointers constructor from raw pointer

It would usually be advisable to use make_unique and make_shared, but both unique_ptr and shared_ptr can be constructed from a raw pointer. Both constructors get the pointer by value and copy it. Both allow (i.e. in the sense of: do not prevent) a continuance usage of the original pointer passed to them in the constructor.

The following code compiles and results with double free:

int* ptr = new int(9);
std::unique_ptr<int> p { ptr };
// we forgot that ptr is already being managed
delete ptr;

Both unique_ptr and shared_ptr could prevent the above if their relevant constructors would expect to get the raw pointer as a const rvalue, e.g. for unique_ptr:

unique_ptr(T* const&& p) : ptr{p} {}

In which case the double free code above would not compile, but the following would:

std::unique_ptr<int> p1 { std::move(ptr) }; // more verbose: user moves ownership
std::unique_ptr<int> p2 { new int(7) };     // ok, rvalue

Note that ptr could still be used after it was moved, so the potential bug is not totally gone. But if user is required to call std::move such a bug would fall into the common rule of: do not use a resource that was moved.


One can ask: OK, but why T* const&& p?

The reason is simple, to allow creation of unique_ptr from const pointer. Remember that const rvalue reference is more generic than just rvalue reference as it accepts both const and non-const. So we can allow the following:

int* const ptr = new int(9);
auto p = std::unique_ptr<int> { std::move(ptr) };

this wouldn't go if we would expect just rvalue reference (compilation error: cannot bind const rvalue to rvalue).


Anyhow, this is too late to propose such a thing. But this idea does present a reasonable usage of an rvalue reference to const.

Martyrology answered 8/3, 2020 at 12:35 Comment(4)
I was amongst people with similar ideas, but I did not have the support to push forward.Angleworm
"auto p = std::unique_ptr { std::move(ptr) };" does not compile with error "class template argument deduction failed". I think it should be "unique_ptr<int>".Dessertspoon
@ZehuiLin the author specifically mentions that this is not how STL is implemented. The language decided to not take this approach.Mor
@SeminPark construction from rvalue pointer does still work (as long as the template params are correct as they are after Zehui's comment), it just isn't required or useful in the actual STL.Margit
P
7

Besides std::ref, the standard library also uses const rvalue reference in std::as_const for the same purpose.

template <class T>
void as_const(const T&&) = delete;

It is also used as return value in std::optional when getting the wrapped value:

constexpr const T&& operator*() const&&;
constexpr const T&& value() const &&;

As well as in std::get:

template <class T, class... Types>
constexpr const T&& get(const std::variant<Types...>&& v);
template< class T, class... Types >
constexpr const T&& get(const tuple<Types...>&& t) noexcept;

This is presumably in order to maintain the value category as well as constness of the wrapper when accessing the wrapped value.

This makes a difference whether const rvalue ref-qualified functions can be called on the wrapped object. That said, I don't know any uses for const rvalue ref qualified functions.

Phenacaine answered 3/5, 2019 at 14:43 Comment(0)
I
6

They are allowed and even functions ranked based on const, but since you can't move from const object referred by const Foo&&, they aren't useful.

Integer answered 8/2, 2011 at 21:58 Comment(12)
What exactly do you mean by the "ranked" remark? Something to do with overload resolution, I guess?Geordie
Why couldn't you move from a const rvalue-ref, if the given type has a move ctor that takes a const rvalue-ref?Kearney
@FredOverflow, the ranking of overload is this: const T&, T&, const T&&, T&&Integer
@Fred Nurk, yes you can, but what sensible actions could that move constructor perform?Integer
@GeneBushuyev: Moving? It is a move ctor. Of course all types won't have this move ctor, just like types don't all have a copy ctor. But the question is any use, not what can you use always.Kearney
@Fred: How do you move without modifying the source?Geordie
@Fred: Mutable data members, or perhaps moving for this hypothetical type doesn't require modifying data members.Kearney
@Fred you could be very evil and copy ¬.¬Uxorial
@Uxorial (bit late to the game, but still), I'd actually say it's even evil to mention that method! A const&& can bind to a const&, so if a const&& is immovable, it should resolve to the const& copying overload. Providing (rather than omitting or deleting) a const&& overload implies that moving it is still possible. Even if well documented this implied promise about the interface is, as you say, "very evil". ;)Spurtle
Of course you can move from it. const means that the object itself is not changed, apart from mutable, static members (e.g. an std::map<T*, T*> that indicates current owner for a type that's aware of its instances) might change. In fact it's useful if you're const-correct since then you might still use the old object.Thompkins
You're allowed to assume that an rvalue reference parameter is the unique reference to the argument object (for the duration of the call) and does not alias other objects. This exclusive access provides the justification for "move" in the non-const case, but even when const the non-aliasing assumption can yield useful optimizations. In particular, operator = (const Foo &&x) does not need to worry about the possibility that &x == this.Paluas
@FredNurk const values are immovable. If you did a move it would result in a copy.Selftaught
D
3

Rvalue references are meant to allow moving data. So in the vast majority of case its use is pointless.

The main edge case you will find it is to prevent people to call a function with an rvalue:

template<class T>
void fun(const T&& a) = delete;

The const version will cover all the edge cases, contrary to the non const version.


Here is why, consider this example:

struct My_object {
    int a;
};

template<class T>
void fun(const T& param) {
    std::cout << "const My_object& param == " << param.a << std::endl;
}

template<class T>
void fun( T& param) {
    std::cout << "My_object& param == " << param.a << std::endl;
}

int main() {

    My_object obj = {42};
    fun( obj );
    // output: My_object& param == 42

    const My_object const_obj = {64};
    fun( const_obj );
    // output: const My_object& param == 64

    fun( My_object{66} );
    // const My_object& param == 66
   
    return 0;
}

Now if you'd like to prevent someone using fun( My_object{66} ); since in the present case, it will be converted to const My_object&, you need to define:

template<class T>
void fun(T&& a) = delete;

And now fun( My_object{66} ); will throw an error, however, if some smarty pants programmer decides to write:

fun<const My_object&>( My_object{1024} );
// const My_object& param == 1024

This will work again and call the const lvalue overload version of that function... Fortunately we can put an end to such profanity adding const to our deleted overload:

template<class T>
void fun(const T&& a) = delete;
Dermato answered 30/9, 2022 at 4:6 Comment(0)
K
2

I can't think of a situation where this would be useful directly, but it might be used indirectly:

template<class T>
void f(T const &x) {
  cout << "lvalue";
}
template<class T>
void f(T &&x) {
  cout << "rvalue";
}

template<class T>
void g(T &x) {
  f(T());
}

template<class T>
void h(T const &x) {
  g(x);
}

The T in g is T const, so f's x is an T const&&.

It is likely this results in a comile error in f (when it tries to move or use the object), but f could take an rvalue-ref so that it cannot be called on lvalues, without modifying the rvalue (as in the too simple example above).

Kearney answered 8/2, 2011 at 22:23 Comment(0)
G
2

It's somewhat disturbing how pretty much everyone in this thread (with the exception of @FredNurk and @lorro) misunderstand how const works, so allow me to chime in.

Const reference only forbids modifying the immediate contents of the class. Not only do we have static and mutable members which we very well can modify through a const reference; but we also can modify the contents of the class stored in a memory location referenced by a non-static, non-mutable pointer - as long as we don't modify the pointer itself.

Which is exactly the case of an extremely common Pimpl idiom. Consider:

// MyClass.h

class MyClass
{
public:
    MyClass();
    MyClass(int g_meat);
    MyClass(const MyClass &&other); // const rvalue reference!
    ~MyClass();

    int GetMeat() const;

private:
    class Pimpl;
    Pimpl *impl {};
};


// MyClass.cpp

class MyClass::Pimpl
{
public:
    int meat {42};
};

MyClass::MyClass() : impl {new Pimpl} { }

MyClass::MyClass(int g_meat) : MyClass()
{
    impl->meat = g_meat;
}

MyClass::MyClass(const MyClass &&other) : MyClass()
{
    impl->meat = other.impl->meat;
    other.impl->meat = 0;
}

MyClass::~MyClass()
{
    delete impl;
}

int MyClass::GetMeat() const
{
    return impl->meat;
}


// main.cpp

const MyClass a {100500};
MyClass b (std::move(a)); // moving from const!
std::cout << a.GetMeat() << "\n"; // returns 0, b/c a is moved-from
std::cout << b.GetMeat() << "\n"; // returns 100500

Behold - a fully functional, const-correct move constructor which accepts const rvalue references.

Gibeon answered 7/2, 2023 at 19:38 Comment(0)
H
0

Perhaps it could be considered useful in this context (coliru link):

#include <iostream>

// Just a simple class
class A {
public:
  explicit A(const int a) : a_(a) {}
  
  int a() const { return a_; }

private:
  int a_;
};

// Returning a const value - shouldn't really do this
const A makeA(const int a) {
    return A{a};
}

// A wrapper class referencing A
class B {
public:
  explicit B(const A& a) : a_(a) {}
  explicit B(A&& a) = delete;
  // Deleting the const&& prevents this mistake from compiling
  //explicit B(const A&& a) = delete;
  
  int a() const { return a_.a(); }
  
private:
  const A& a_;
};

int main()
{
    // This is a mistake since makeA returns a temporary that B
    // attempts to reference.
    auto b = B{makeA(3)};
    std::cout << b.a();
}

It prevents the mistake being compiled. There are clearly a bunch of other problems with this code that compiler warnings do pick up, but perhaps the const&& helps?

Halicarnassus answered 25/8, 2022 at 11:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.