对 const 的右值引用有什么用吗?

Do rvalue references to const have any use?

提问人:fredoverflow 提问时间:2/9/2011 最后编辑:David Gfredoverflow 更新时间:2/9/2023 访问量:24269

问:

我想不是,但我想确认一下。有什么用吗,类类型在哪里?const Foo&&Foo

C++ C++11 常量 rvalue-reference

评论

2赞 Aaron McDaid 10/6/2016
在这段视频中,STL说这非常重要,尽管他没有说为什么: youtube.com/watch?v=JhgWFYfdIho#t=54m20sconst&&

答:

6赞 Gene Bushuyev 2/9/2011 #1

它们是允许的,甚至函数的排名基于 ,但由于你不能从 引用的 const 对象中移动,所以它们没有用。constconst Foo&&

评论

0赞 fredoverflow 2/9/2011
你所说的“排名”评论到底是什么意思?我猜与过载分辨率有关吗?
0赞 Fred Nurk 2/9/2011
如果给定类型有一个采用 const rvalue-ref 的移动 ctor,为什么你不能从 const rvalue-ref 移动?
6赞 Gene Bushuyev 2/9/2011
@FredOverflow,过载的排名是这样的:const T&, T&, const T&&, T&&
2赞 fredoverflow 2/9/2011
@Fred:如何在不修改源的情况下移动?
3赞 Fred Nurk 2/9/2011
@Fred:可变数据成员,或者可能针对此假设类型移动不需要修改数据成员。
2赞 Fred Nurk 2/9/2011 #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 是 T 常量,所以 f 的 x 是 T 常量&&。

这可能会导致 f 中的 comile 错误(当它尝试移动或使用对象时),但 f 可以采用 rvalue-ref,这样它就不能在左值上调用,而无需修改右值(如上面太简单的示例)。

97赞 Howard Hinnant 2/9/2011 #3

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

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

上述两个重载确保另一个 and 函数不会绑定到右值(否则这是可能的)。ref(T&)cref(const T&)

更新

我刚刚检查了官方标准 N3290,不幸的是它没有公开可用,它在 20.8 中具有 Function 对象 [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 7/15/2013
为什么你在回答中包含了三次相同的代码?我试图找到差异太久了。
12赞 Howard Hinnant 7/15/2013
@typ1232:看来我在回答答案近 2 年后更新了答案,因为评论中担心引用的功能不再出现。我从 N3290 和当时最新的草稿 N3485 进行了复制/粘贴,以显示这些功能仍然出现。在我当时的脑海中,使用复制/粘贴是确保比我更多的眼睛可以确认我没有忽略这些签名中的一些微小变化的最佳方法。
3赞 filipos 5/5/2016
验证两个片段的等效性的过程:将第一个片段复制到文本文件中;将不相关的代码片段复制到第二个文本文件中;将第二个代码片段复制到第三个文本文件中。用于验证第一个文件和最后一个文件是否相等,以及第二个文件是否不同。diff
3赞 Howard Hinnant 1/31/2018
使用 of 可以防止有人愚蠢地使用形式的显式模板参数。这不是一个非常有力的论点,但过度的成本是相当低的。const T&&ref<const A&>(...)const T&&T&&
3赞 Secundi 3/29/2021
@OliverSeiler不,你错了。转发引用是对 cv 非限定模板参数的右值引用。由于 const 限定符,这里的情况并非如此,即它们的行为不像转发引用,而始终只是示例中对 T 的常量右值引用。
6赞 eerorika 5/3/2019 #4

除了 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 rvalue ref 限定函数的任何用途。

27赞 Amir Kirsh 3/8/2020 #5

获取常量右值引用(而不是 for)的语义是说:=delete

  • 我们不支持左值的操作!
  • 即使如此,我们仍然会复制,因为我们无法移动传递的资源,或者因为“移动”它没有实际意义。

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


案例:来自原始指针的智能指针构造函数

通常建议使用 和 ,但两者都可以从原始指针构造。两个构造函数都按值获取指针并复制它。两者都允许(即:不阻止)在构造函数中传递给它们的原始指针的连续使用。make_uniquemake_sharedunique_ptrshared_ptr

以下代码编译并生成 double free

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

如果它们的相关构造函数希望将原始指针作为常量右值获取,则两者都可以防止上述情况,例如:unique_ptrshared_ptrunique_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

请注意,移动后仍然可以使用,因此潜在的错误并没有完全消失。但是,如果要求用户调用这样的错误,则属于以下常见规则:不要使用已移动的资源。ptrstd::move


有人可能会问:好吧,但为什么要坚持呢?T* && p

原因很简单,允许创建 from const 指针。请记住,常量右值引用比右值引用更通用,因为它同时接受 和 。因此,我们可以允许以下内容:unique_ptrconstnon-const

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

如果我们只期望右值引用(编译错误:无法将常量右值绑定到右值),则不会这样做。


无论如何,现在提出这样的事情为时已晚。但这个想法确实提出了对 const 的右值引用的合理用法。

评论

1赞 Red.Wave 3/8/2020
我和有类似想法的人在一起,但我没有得到支持来推动。
0赞 Zehui Lin 8/11/2020
“auto p = std::unique_ptr { std::move(ptr) };” 无法编译,并出现错误“类模板参数推导失败”。我认为它应该是“unique_ptr<int>”。
0赞 Semin Park 1/12/2023
@ZehuiLin作者特别提到这不是STL的实现方式。该语言决定不采用这种方法。
0赞 Ryan McCampbell 5/20/2023
@SeminPark值指针的构造仍然有效(只要模板参数是正确的,因为它们在 Zehui 的评论之后),它只是在实际的 STL 中不是必需的或有用的。
0赞 Rai 8/25/2022 #6

也许在这种情况下可以认为它是有用的(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&&

1赞 arkan 9/30/2022 #7

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

您会发现它的主要边缘情况是防止人们调用带有右值的函数:

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

现在,如果你想阻止有人使用,因为在本例中,它将被转换为 const My_object&,你需要定义:fun( My_object{66} );

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 添加到我们删除的重载中:

template<class T>
void fun(const T&& a) = delete;
0赞 ScumCoder 2/8/2023 #8

有点令人不安的是,这个线程中的几乎每个人(除了 @FredNurk 和 @lorro)都误解了工作原理,所以请允许我插话。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

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