为什么 std::move 可以移动堆栈数据对象?

Why std::move can move a stack data object?

提问人:CPW 提问时间:11/7/2023 更新时间:11/7/2023 访问量:133

问:

我对以下关于移动堆栈对象的代码感到困惑:

    int *pOuter = nullptr;
    {
        int j;
        std::cout << "top of the inner stack=" << &j << std::endl;
        int aData[] = {1,2,3,4,5};  // in a vanishing stack
        aData[0] = 0;  // to make sure that aData is on stack
        int *pInner = aData;
        std::cout << "Before Move: pOuter=" << pOuter << ", pInner=" << pInner << std::endl;
        pOuter = std::move(pInner);
        std::cout << "After Move: pOuter=" << pOuter << ", pInner=" << pInner << std::endl;
    }
    // aData[0] and pInner are no longer accessible
    int i;
    std::cout << "top of the outer stack=" << &i << std::endl;
    int aCover[] = {10,9,8,7,6,5,4,3,2,1};  // to reclaim stack
    std::cout << "aCover=" << aCover << ", pOuter=" << pOuter << ": ";
    for (i = 0; i < 5; ++i) {
        std::cout << *(pOuter + i) << ",";
    }
    std::cout << std::endl;

输出为:

top of the inner stack=0xb2125ff7cc
Before Move: pOuter=0, pInner=0xb2125ff7b0
After Move: pOuter=0xb2125ff7b0, pInner=0xb2125ff7b0
top of the outer stack=0xb2125ff804
aCover=0xb2125ff7d0, pOuter=0xb2125ff7b0: 0,2,3,4,5,

我很惊讶它为什么会起作用,因为堆栈对象 sData 应该被 aCover 销毁并覆盖,这样 pOuter 应该会导致分段失败,或者至少指向 sCover。相反,aData 不会与内部堆栈一起销毁。

从显示的地址来看,我的猜测是 C++ 编译器实际上在内部堆栈之前分配了以下堆栈对象:

int i; 
int aCover[] = {10,9,8,7,6,5,4,3,2,1};

即便如此,在我看来,pOuter 是一个悬而未决的参考,它的工作是偶然的。

我说得对吗?

C++ 移动语义

评论

4赞 NathanOliver 11/7/2023
您有未定义的行为。 指向垃圾,因此无法取消引用它。pOuter
6赞 Remy Lebeau 11/7/2023
std::move只是一个类型转换,仅此而已。对于基元类型(如指针),移动与复制相同,因此与将外部指针设置为指向超出范围的本地数据相同,从而使外部指针悬空。pOuter = std::move(pInner);pOuter = pInner;
1赞 n. m. could be an AI 11/7/2023
std::move并没有真正移动任何东西。这是一个演员表。移动赋值运算符和移动构造函数确实会移动,但普通指针缺少其中任何一个,因此代码中不会发生任何移动。
1赞 463035818_is_not_an_ai 11/7/2023
这不“行”。错误的代码可以产生任何结果,也会产生与正确代码相同的结果。未定义的行为是未定义的。重要的是,您可以从没有未定义行为的代码中获得有保证的行为。具有未定义行为的代码可以产生看起来没问题的输出,这没有多大价值,因为一旦你打开了优化或更改编译器标志或其他什么,结果可能会完全不同
1赞 Eljay 11/7/2023
为什么 std::move 可以移动堆栈数据对象?它不能。事实并非如此。std::move

答:

3赞 Paul Sanders 11/7/2023 #1

正如评论中所指出的,只是一个类型转换。那么它是如何工作的呢?std::move

好吧,让我们以下面的代码作为一个简单的例子:

Foo x;
Foo y = x;

这制作了 in 的副本,即它调用 的 copy 构造函数来复制 。xyyx

鉴于这:

Foo x;
Foo y = std::move (x);

进入 ,如果您不再关心,则效率更高。xyx

但是 - 这是关键 - 只有在有合适的移动构造函数的情况下才做任何有用的事情,其签名将是:std::moveFoo

Foo &Foo (Foo &&other)

即参数必须是 ,并且本质上是强制转换为右值(以左值开始),从而告诉编译器调用移动构造函数而不是复制构造函数。otherrvaluestd::movex

仅此已。它本身不会移动任何东西。怎么可能?它不知道该怎么做,如果没有合适的移动构造函数存在,编译器将回退到复制。特别是基元类型的情况。std::movex

评论

0赞 CPW 11/7/2023
在 C++11 及更高版本中,可以移动返回或函数参数列表中的 temp 对象(所谓的纯右值)。这可能是通过调整堆栈指针来完成的,也可能是通过调整参数列表的推送顺序来完成的,所以我猜它不仅仅是一个简单的强制转换。对于我创建的案例,我非常惊讶堆栈以这样一种方式重新排列,使移动成为可能。我想知道编译器是否会查看 std::move() 目标并进行此类调整。
0赞 CPW 11/7/2023
在 C++11 及更高版本中,以下代码不会生成副本: A f() { return A();A a = f();原因似乎是当 f() 退出时,堆栈指针不会倒带,因此可以将 f() 中的数据对象 A 分配给 a。所以它似乎不是一个纯粹的类型演员。
1赞 xaxxon 11/7/2023
根据您在上面评论中对术语的使用,您仍然不了解 std::move 的作用。这只是一个演员表。就是这样。它对堆栈或堆一无所知,堆栈或堆也与它无关。
1赞 Paul Sanders 11/7/2023
@CPW 你的这些评论离谱太远了。只要假装(因为这是真的)在你发布的代码中根本没有做任何事情,也许然后你会看到它。std::move
1赞 xaxxon 11/7/2023
在你意识到“std::move不会移动任何东西”之前,你需要继续阅读并停止打字,因为你只会让自己更加困惑。std::move 不动。