常量的右值引用有什么用处吗?

122

我猜不会有用,但我想确认一下。对于一个类类型 Foo,是否有使用 const Foo&& 的情况?


2
在这个视频中,STL说const&&非常重要,尽管他没有解释为什么:https://www.youtube.com/watch?v=JhgWFYfdIho#t=54m20s - Aaron McDaid
8个回答

97

它们偶尔会有用。C++0x草案本身在一些地方使用它们,例如:

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

上述两个重载函数确保其他ref(T&)cref(const T&)函数不会绑定到rvalue(否则可能会发生)。

更新

我刚刚查了官方标准N3290,不幸的是它并不是公开的,在20.8函数对象[function.objects]/p2中提到:

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

然后我查看了最新的C++11草案,它是公开可用的,N3485,在20.8 函数对象 [function.objects]/p2中仍然写着:

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

96
为什么你在回答中三次包含了相同的代码?我试图找到区别太久了。 - typ1232
12
似乎我在回答后将近两年更新了答案,原因是有评论提出参考的函数已不再存在。我从N3290和当时最新的草案N3485中进行了复制/粘贴,以表明这些函数仍然存在。在我当时的想法中,使用复制/粘贴是确保比我更多的人能够确认我没有忽略这些签名中的一些细微变化的最佳方式。 - Howard Hinnant
3
验证两个代码片段等价的步骤:将第一个代码片段复制到一个文本文件中;将一个不相关的代码片段复制到第二个文本文件中;将第二个代码片段复制到第三个文本文件中。使用 diff 命令来验证第一个和最后一个文件相等,而第二个文件则不同。 - filipos
3
使用const T&&可以防止某些人愚蠢地使用形式为ref<const A&>(...)的显式模板参数。这不是一个非常有力的论点,但相比于T&&而言,const T&&的成本非常小。 - Howard Hinnant
3
不,你错了。转发引用是一个对于cv未限定模板参数的右值引用。由于const约束条件,这不适用于此处,也就是说它们不像转发引用一样行事,而仅仅是T的常量右值引用。 - Secundi
显示剩余9条评论

27
获取const右值引用(不是=delete)的语义是为了表明:
  • 我们不支持lvalues的操作!
  • 尽管如此,我们仍然复制,因为我们无法移动传递的资源,或者因为没有“移动”它的实际含义。

以下用例可能是rvalue引用到const的一个很好的用例,尽管语言决定不采用这种方法(请参见原始SO帖子)。


情况:智能指针从裸指针构造函数

通常建议使用make_uniquemake_shared,但unique_ptrshared_ptr都可以从裸指针构造。两个构造函数都按值获取指针并复制它。两者都允许(即以不防止的意义)在构造函数中传递的原始指针的继续使用。

以下代码编译并导致双重释放

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

unique_ptrshared_ptr都可以避免上述问题,只需要它们的相关构造函数接受作为const右值的原始指针,例如对于unique_ptr

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

如果情况是这样的,上面的双重释放代码将不会编译,但以下代码会:
std::unique_ptr<int> p1 { std::move(ptr) }; // more verbose: user moves ownership
std::unique_ptr<int> p2 { new int(7) };     // ok, rvalue

请注意,即使移动后,ptr仍然可以使用,因此潜在的错误并没有完全消失。但是如果要求用户调用std::move,这样的错误将遵循常规规则:不要使用已移动的资源。
有人可能会问:好的,但为什么是T* const&& p
原因很简单,是为了允许从const pointer创建unique_ptrconst rvalue referencervalue reference更加通用,因为它接受constnon-const。所以我们可以允许以下操作:
int* const ptr = new int(9);
auto p = std::unique_ptr<int> { std::move(ptr) };

如果我们只期望rvalue引用,那么这个代码就无法编译通过(编译错误:无法将const rvalue绑定到rvalue)。


不管怎样,现在提出这样的想法已经太晚了。但是这个想法确实展示了对rvalue引用到const的合理使用。


1
我曾经和有着相似想法的人在一起,但是我没有得到推进的支持。 - Red.Wave
“auto p = std::unique_ptr { std::move(ptr) };" 的错误提示为 "类模板参数推断失败"。我认为应该更改为 "unique_ptr<int>"。 - Zehui Lin
@ZehuiLin 作者特别提到这不是STL的实现方式。该语言决定不采用这种方法。 - Semin Park
@SeminPark 从rvalue指针构造仍然“可以工作”(只要模板参数像在Zehui的评论后一样正确),但它在实际的STL中不是必需的或有用的。 - Ryan McCampbell

6

除了std::ref,标准库在std::as_const中也使用const右值引用来实现相同的目的。

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

当获取被包装的值时,它也用作std::optional的返回值:

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

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;

这可能是为了在访问封装值时保持值类别和const性质的稳定。

这会影响是否可以调用封装对象上的const rvalue ref限定函数。话虽如此,我不知道const rvalue ref限定函数有任何用途。


6

它们可以根据const进行排名,甚至可以使用,但由于您无法从const Foo&&所引用的常量对象中移动,因此它们没有用处。


6
@FredOverflow,函数重载的优先级为:const T&、T&、const T&&、T&& - Gene Bushuyev
2
@Fred:你怎么在不修改源码的情况下进行移动? - fredoverflow
3
可变数据成员,或者对于这种假设类型的移动可能不需要修改数据成员。 - Fred Nurk
@KitsuneYMG(可能有点晚了,但还是要说),我实际上会说提到那个方法甚至是邪恶的!const&&可以绑定到const&,因此如果const&&是不可移动的,则应解析为const&复制重载。提供(而不是省略或删除)const&&重载意味着仍然可能将其移动。即使有很好的文档记录,关于接口的这种暗示承诺也是你所说的“非常邪恶”。;) - monkey0506
当然可以移动它。const 意味着对象本身不会改变,除了可变的静态成员(例如 std::map<T*, T*>,用于指示知道其实例的类型的当前所有者)可能会改变。事实上,如果您是 const-correct,则很有用,因为那么您仍然可以使用旧对象。 - lorro
显示剩余7条评论

2

我想不出这种情况直接有用的场景,但它可能会间接使用:

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);
}

g中的T是一个常量,所以f的x也是一个T常量,并且。。。

这很可能会导致f在尝试移动或使用对象时出现编译错误,但f可以采用rvalue-ref方式,因此它不能被调用为lvalue,而不修改rvalue(如上面太简单的例子)。


1

Rvalue引用旨在允许移动数据。因此,在绝大多数情况下,它的使用是无意义的。

你会发现主要的边缘情况是为了防止人们使用rvalue调用函数:

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

与非 const 版本相比,const 版本将涵盖所有的边界情况。


这就是为什么,考虑这个例子:

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;
}

现在,如果你想要防止有人使用fun( My_object{66} );,因为在当前情况下,它将被转换为const My_object&,你需要定义:

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

现在,fun( My_object{66} );将会抛出一个错误,但是如果有些聪明的程序员决定写:

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

这将再次起作用并调用该函数的const lvalue重载版本...幸运的是,我们可以通过向删除的重载添加const来结束这种亵渎:

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

0

或许在这种情境下(coliru 链接)可以被认为是有用的:

#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();
}

它可以防止错误被编译。这段代码还有其他一堆问题,编译器警告会捕捉到,但也许 const&& 有所帮助?


0

这个帖子中几乎每个人(除了@FredNurk和@lorro)都误解了const的工作原理,这有点令人不安,所以让我来解释一下。

只有const引用禁止修改类的直接内容。我们不仅有静态和可变成员,可以通过const引用进行修改;而且我们还可以修改存储在非静态、非可变指针引用的内存位置中的类的内容——只要我们不修改指针本身。

这正是一个极为常见的Pimpl惯用法。考虑以下情况:

// 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

看啊 - 一个完全功能的、常量正确的移动构造函数,接受常量右值引用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接