将 std::move shared_ptr与条件运算符一起使用时出现奇怪的行为

Weird behavior when using std::move shared_ptr with conditional operator

提问人:danry 提问时间:5/9/2023 最后编辑:cigiendanry 更新时间:5/9/2023 访问量:148

问:

我正在使用 on 处理一些 C++ 代码,并得到了非常奇怪的输出。我简化了我的代码,如下所示std::moveshared_ptr

int func(std::shared_ptr<int>&& a) {
    return 0;
}

int main() {
    std::shared_ptr<int> ptr = std::make_shared<int>(1);

    for (int i = 0; i != 10; ++i) {
        func(i == 9 ? std::move(ptr) : std::shared_ptr<int>(ptr));
    }

    if (ptr) {
        std::cout << "ptr is not null: " << *ptr << "\n";
    } else {
        std::cout << "ptr is null\n";
    }

    return 0;
}

我得到了输出

ptr is null

正如我所料,我的意志在最后一个循环中被移动(强制转换为),并且由于从不窃取内存,我的外部将是非空的(结果实际上是空的)。如果我替换ptrstd::shared_ptr<int>&&funcaptr

func(i == 9 ? std::move(ptr) : std::shared_ptr<int>(ptr));

使用 if-else 语句

if (i == 9) func(std::move(ptr));
else func(std::shared_ptr<int>(ptr));

输出将是

ptr is not null: 1

我对编译器的这种行为感到非常困惑。

我尝试了具有不同 std 版本和优化级别的 GCC 和 clang,并得到了相同的输出。有人可以为我解释为什么以及下面的数据在哪里被盗吗?ptr

C++ shared-ptr move-semantics 条件运算符 move-constructor

评论

0赞 Adrian Maire 5/9/2023
UB不是在对象被移动后使用它吗?
5赞 François Andrieux 5/9/2023
我认为条件运算符的两个分支的常见类型是,在第一种情况下,构造了一个移动构造的临时实例,并在调用函数之前将其传递给该实例窃取了该实例的所有权。std::shared_ptr<int>funcptr
0赞 AF_cpp 5/9/2023
这是有道理的。您正在进入 func,而 func 只是破坏了临时参数。最后一个迭代会将shared_ptr移动到函数中。在功能结束时,“搬进来”shared_ptr被摧毁
1赞 François Andrieux 5/9/2023
@AF_cpp 的参数是参考。只有引用的生存期结束在 的末尾,而不是被引用的对象(除非引用恰好延长了具体化临时对象的生存期)。funcfunc
2赞 konchy 5/9/2023
上面条件表达式的类型是 ,所以在第一种情况下,当条件被击中时,会构造一个临时对象,勾选 godbolt.org/z/44EM6YPadstd::shared_ptr<int>

答:

6赞 Brian Bi 5/9/2023 #1

由于条件运算符的第二个和第三个操作数不具有相同的值类别(即,是 xvalue,而 prvalue),因此此条件表达式属于 [expr.cond]/7std::move(ptr)std::shared_ptr<int>(ptr)

左值到右值、数组到指针和函数到指针的标准转换在第二和第三操作数上执行。 在这些转换之后,以下其中一项应成立:

  • 第二和第三操作数具有相同的类型;结果是该类型的,并且使用选定的操作数初始化结果对象。
  • [...]

std::shared_ptr<int>(ptr)已经是一个 PRVALUE,因此左值到右值的转换(实际上是 glvalue 到 PRVALUE 的转换)对它没有任何作用。

std::move(ptr)转换为 PR值,并且该 PR值用于初始化结果对象。结果对象的初始化使用 move 构造函数(因为这是从该类型的 xvalue 初始化 a 的构造函数,这就是它)。move 构造函数从 中“窃取”值。result 对象是一个临时对象,它绑定到参数,然后被销毁。请注意,所有这些都只发生在实际评估的情况下(这需要为真)。std::shared_ptr<int>std::move(ptr)ptrastd::move(ptr)i == 9

评论

0赞 Red.Wave 5/10/2023
但是,我想这是一种潜在的 DR. 常见类型的值,它的参考应该是参考恕我直言。
0赞 Brian Bi 5/10/2023
@Red.Wave 这将改变现有代码的含义,并且会强制实现引入一个隐藏的运行时标志,用于确定是否存在需要销毁的临时对象,因为临时对象只会沿着条件表达式的一个分支创建。
0赞 Red.Wave 5/10/2023
如果使用结果,它将绑定到引用。不需要启发式方法,只需延长寿命即可暂时 - 无需考虑是否使用过。
0赞 Brian Bi 5/10/2023
@Red.Wave:我不明白你的意思。如果你的建议是整个条件表达式的结果应该是一个 xvalue,那么这意味着当第二个操作数被选中时,它将成为结果(不是临时的),当第三个操作数被选中时,一个临时的被具体化。因此,您将有条件地创建临时。在第一种情况下,没有暂时的延长寿命。
1赞 Brian Bi 5/11/2023
@Red.Wave:如果首先没有评估第三个操作数,则无法延长第三个操作数的生存期。条件运算符仅计算第二个或第三个操作数,而不是同时计算这两个操作数。
2赞 Kai Petzke 5/9/2023 #2

调用的 C++ 函数不需要移出值,即使使用右值引用调用它们也是如此。特别是,你甚至没有触及它的参数,所以在这个调用中保持不变:。func()aptrfunc(std::move(ptr))

相反,你的表情

func(i == 9 ? std::move(ptr) : std::shared_ptr<int>(ptr));

始终在调用之前创建一个 temporary 类型,并使用对该 temporary 的右值引用。引擎盖下发生的事情是:std::shared_ptr<int>func()

{
    std::shared_ptr<int> temp = i == 9 ? std::move(ptr) : std::shared_ptr<int>(ptr));
    func(std::move(temp));
}

当为 9 时,赋值将移至空并留空。然后,当到达块的末尾并且临时超出范围时,它就会被销毁。itemp = std::move(ptr)ptrtempptr

您可以通过将行更改为以下两种变体之一来避免这种行为:

func(i == 9 ? std::move(ptr) : std::move(std::shared_ptr<int>(ptr)));

艺术

func(std::move(i == 9 ? ptr : std::shared_ptr<int>(ptr)));

这两种变体都确保临时创建的是引用类型(即在第一种情况下和在第二种情况下),因此不需要复制任何内容。std::shared_ptr<int>&&std::shared_ptr<int>&

你可能会问,为什么原始代码会创建一个临时类型(而不是 )。嗯,原因是众所周知的返回值优化(RVO),它在几十年前被引入C++:返回对象的函数实际上是以调用函数已经为该对象分配空间并将指向该对象的指针传递给被调用函数的方式调用的。然后,被调用的函数将返回值直接写入该空间。正因为如此,许多直接任务,例如:std::shared_ptr<int>std::shared_ptr<int>&&

auto x = function(a, b, c);

省去了先创建临时值然后立即再次销毁临时值的麻烦。但是在你的三元运算符的情况下,原本隐藏的临时突然变得栩栩如生。

评论

0赞 danry 5/10/2023
我认为(并测试)在第二个变体中,三元的常见类型是而不是 ,这会导致最后一个循环的复制构造。第一个变体是可以的。 谢谢你的改进建议!std::shared_ptr<int>std::shared_ptr<int>&