提问人:fredoverflow 提问时间:2/9/2011 最后编辑:David Gfredoverflow 更新时间:2/9/2023 访问量:24269
对 const 的右值引用有什么用吗?
Do rvalue references to const have any use?
答:
它们是允许的,甚至函数的排名基于 ,但由于你不能从 引用的 const 对象中移动,所以它们没有用。const
const Foo&&
评论
const T&, T&, const T&&, T&&
我想不出这直接有用的情况,但它可能会间接使用:
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,这样它就不能在左值上调用,而无需修改右值(如上面太简单的示例)。
它们偶尔有用。草案 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;
评论
diff
const T&&
ref<const A&>(...)
const T&&
T&&
除了 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 限定函数的任何用途。
获取常量右值引用(而不是 for)的语义是说:=delete
- 我们不支持左值的操作!
- 即使如此,我们仍然会复制,因为我们无法移动传递的资源,或者因为“移动”它没有实际意义。
恕我直言,以下用例可能是右值引用 const 的一个很好的用例,尽管该语言决定不采用这种方法(参见原始 SO 帖子)。
案例:来自原始指针的智能指针构造函数
通常建议使用 和 ,但两者都可以从原始指针构造。两个构造函数都按值获取指针并复制它。两者都允许(即:不阻止)在构造函数中传递给它们的原始指针的连续使用。make_unique
make_shared
unique_ptr
shared_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_ptr
shared_ptr
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*
&& p
原因很简单,允许创建 from const 指针。请记住,常量右值引用比右值引用更通用,因为它同时接受 和 。因此,我们可以允许以下内容:unique_ptr
const
non-const
int* const ptr = new int(9);
auto p = std::unique_ptr<int> { std::move(ptr) };
如果我们只期望右值引用(编译错误:无法将常量右值绑定到右值),则不会这样做。
无论如何,现在提出这样的事情为时已晚。但这个想法确实提出了对 const 的右值引用的合理用法。
评论
也许在这种情况下可以认为它是有用的(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&&
右值引用旨在允许移动数据。 因此,在绝大多数情况下,它的使用是没有意义的。
您会发现它的主要边缘情况是防止人们调用带有右值的函数:
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;
有点令人不安的是,这个线程中的几乎每个人(除了 @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 - 一个功能齐全的、常量正确的移动构造函数,它接受常量右值引用。
评论
const&&