C++11中rvalue引用数组的目的是什么?

5
在C++03和C++11中,数组不能通过值从函数返回(只能通过引用/常量引用返回)(因为我们无法直接将一个数组赋值给另一个数组)。
const size_t N = 10;
using Element = int;
using Array = Element[N];

Array array;

// does not compile
// Array GetArray()
// {
//     return array;
// }

Array& GetArrayRef()
{
    return array;
}

C++引入了一种新的引用类型——右值引用。它也可以与数组一起使用:

void TakeArray(Array&& value)
{
}

// ...

TakeArray(std::forward<Array>(array));
TakeArray(std::forward<Array>(GetArrayRef()));

这样的引用(rvalue reference to an array)有什么作用?它能在实际代码中使用吗,还是只是C++11标准的缺陷?

3
你的第一个假设并不完全正确。在C++11中,你可以定义一个std::array<Element, N>,它可以通过值返回。 - MatthiasB
2
@MatthiasB std::array 不是一个数组,它是一个 std 容器。 - Constructor
我不知道。假设数组不过是使用指针的一种好方式,那么你基本上是在编写 TakeArray((int*)&& value) - MatthiasB
这与任何其他移动操作并没有不同。您不应该期望一个指向对象的指针去移动实际的对象,那么为什么代码在处理数组时会有所不同呢? - MatthiasB
@MatthiasB 因为你不能像我在答案中所写的那样从函数返回数组。 - Constructor
显示剩余6条评论
4个回答

5
一种右值引用是程序员或编译器承诺,被引用的数据即将被处理1,如果通过合理的方式可以提高读取速度(等等),则更改它的读者是可接受的。
如果您从具有上述承诺的vector数组中复制,您可以清楚地move所包含的vector,这可能会大幅提升性能。
所以,不,这不是一个缺陷。
std::vector<int> data[1000];
void populate_data() {
  for( auto& v : data )
    v.resize(1000);
}
template<std::size_t N>
std::vector< std::vector<int> > get_data( std::vector<int>(&&arr)[N] ) {
  std::vector< std::vector<int> > retval;
  retval.reserve(N);
  for( auto& v : arr )
    retval.emplace_back( std::move(v) );
  return retval;
}
template<std::size_t N>
std::vector< std::vector<int> > get_data( std::vector<int>(const&arr)[N] ) {
  std::vector< std::vector<int> > retval;
  retval.reserve(N);
  for( auto const& v : arr )
    retval.emplace_back( v );
  return retval;
}
int main() {
  populate_data();
  auto d = get_data(data); // copies the sub-vectors
  auto d2 = get_data( std::move(data) ); // moves the sub-vectors
}

在这种情况下,虽然有点矫揉造作,但通常数据源可以被移动或不移动。如果整个容器被移动(rvalue引用),则rvalue覆盖会移动子数据;否则进行复制。

稍加修改,我们可以将上述内容写成一个方法。因此,如果我们有一个rvalue容器(即拥有范围),而容器本身不适合直接从中移动,我们就会移动其内容。但这只是高级元编程,并非答案的关键。

1编译器在涉及到对象在使用后“不可能”被引用(因为它是匿名临时对象或者被用于函数返回值的某些情况)的受限情况下执行这项操作。(实际上并不是完全不可能,理论上这个C++11的特性可能会影响pre-C++11的代码,但是影响很小)。程序员可以通过rvalue转换来完成这个操作。它被“立即处理”的具体含义取决于上下文,但一般规则是rvalue引用的使用者可以将对象放入任何可以“有效”销毁和交互的状态中。基于rvalue的swap存在的原因是将其放入仅可销毁的状态不安全。

你能举一个代码示例来说明你的话吗? - Constructor
非常感谢!这很有趣,我会考虑如何在一个函数中实现它。 - Constructor
@Constructor 会变得混乱。你的函数签名将变成 template<typename C>void get_data( C&& c )。你需要一些辅助函数来回答诸如“这是一个拥有容器吗”、“如果有有效的方法,获取此视图的大小,否则为0”,以及 "move_if<bool>" (或 make_move_iterator_if<bool>),然后创建一个 constexpr bool move_from = std::is_rvalue_ref< C&& >::value 来控制移动或不移动。概念后期事情会变得更容易。 - Yakk - Adam Nevraumont
是的,我也认为它可以以某种方式实现。 - Constructor
在rvalue重载的foreach循环中,auto& v 不应该是 auto&& v 吗?或者我对rvalue的工作方式有误解? - pro-gramer
@pro-g 是的,你误解了。arr是一个右值引用,但作为表达式时是左值。这很棘手,但几乎在每个上下文中,命名的东西都是左值(已添加一些例外)。 - Yakk - Adam Nevraumont

3

并不完全正确地说,你不能有数组临时变量。但是你可以。

void f(const std::string(&&x)[2]) {
   std::cout << "rvalue" << std::endl;
}

void f(const std::string(&x)[2]) {
   std::cout << "lvalue" << std::endl;
}

template<typename T>
using id = T;

int main() {
  f(id<std::string[]>{"Hello", "Folks"});
  f(id<std::string const[]>{"Hello", "Folks"});

  std::string array[] = { "Hello", "Folks" };
  std::string const arrayConst[] = { "Hello", "Folks" };
  f(array);
  f(arrayConst);
}

从C++14开始,您也可以直接将花括号初始化列表传递给右值重载函数

f({ "Hello", "Folks" });

太好了!但是可以不使用别名模板来实现:f((std::string[]){"Hello", "Folks"});。你想在第二对 f 函数调用中展示什么? - Constructor
@Constructor 可以区分左值和右值。第二对调用将调用第二个 f,因为它们是左值。与第一对调用相反。如果没有别名模板或typedef,您无法做到这一点。 - Johannes Schaub - litb
哦,我现在明白了,你拿这段代码来展示rvalue和lvalue数组调用不同的重载函数。关于f((std::string[]){"Hello", "Folks"});:为什么这段代码在clang和gcc下都能编译通过?这是编译器的一个bug吗? - Constructor
@Constructor 是一个 C99 复合字面量,不是有效的 C++ 语法。它甚至具有不同的语义,因为数组的元素将在调用作用域块中具有生命周期(除非编译器选择以不同于 C99 描述的方式实现 C++ 扩展)。 - Johannes Schaub - litb
谢谢。我忘记了-pedantic-errors标志。 :-) - Constructor

2

右值引用有两个含义:

  • 它是一个引用,具备所有引用的特性
  • 它向编译器保证所引用对象是一个未命名(临时)对象或可视为这样的对象

C++中,通过引用传递数组,可以确保其长度,这已经比C语言的void func(int a[5])要好了很多,因为在这种情况下,传递长度为2的数组是完全可以接受的(尽管良好的编译器会发出警告)。

然而,对于数组的右值引用的好处不太明显:

  • 未命名对象不能别名化;因此,右值引用必然不会被别名化,就像使用了restrict一样
  • 调用者会被告知数组元素可能会被移动

因此,即使是数组,也存在优化的可能。


如何获取未命名的数组? - Constructor
struct A { int array[5]; }; + A func(); + call(func().array) 的例子中,func() 返回的值是无名的,因此 func().array 也是无名的。 - Matthieu M.

1
这并不算是一个缺陷,它引入了令人兴奋的能力,可以将对象移动到目标位置。这意味着目标句柄将被交换为即将离开作用域的源。

请您能否给出一个示例代码,以证明您所说的应用右值引用到数组的方法? - Constructor

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