ChatGPT解决这个技术问题 Extra ChatGPT

从函数返回 unique_ptr

unique_ptr<T> 不允许复制构造,而是支持移动语义。然而,我可以从一个函数返回一个 unique_ptr<T> 并将返回的值分配给一个变量。

#include <iostream>
#include <memory>

using namespace std;

unique_ptr<int> foo()
{
  unique_ptr<int> p( new int(10) );

  return p;                   // 1
  //return move( p );         // 2
}

int main()
{
  unique_ptr<int> p = foo();

  cout << *p << endl;
  return 0;
}

上面的代码按预期编译和工作。那么,1 行如何不调用复制构造函数并导致编译器错误呢?如果我必须改用第 2 行,那将是有意义的(使用第 2 行也可以,但我们不需要这样做)。

我知道 C++0x 允许 unique_ptr 出现这个异常,因为返回值是一个临时对象,一旦函数退出就会被销毁,从而保证返回指针的唯一性。我很好奇这是如何实现的,它在编译器中是特殊情况还是语言规范中是否有其他条款可以利用?

假设您正在实现一个工厂方法,您希望 1 还是 2 来返回工厂的输出?我认为这将是 1 最常见的用法,因为通过适当的工厂,您实际上希望将构造事物的所有权传递给调用者。
@Xharlie?它们都传递了 unique_ptr 的所有权。整个问题是关于 1 和 2 是实现同一目标的两种不同方式。
在这种情况下,RVO 也发生在 c++0x 中,unique_ptr 对象的销毁将在 main 函数退出后执行一次,但不会在 foo 退出时执行。

C
Curious

语言规范中是否还有其他条款可以利用?

是的,参见 12.8 §34 和 §35:

当满足某些条件时,允许实现省略类对象的复制/移动构造 [...] 这种复制/移动操作的省略,称为复制省略,在返回语句中被允许 [...]具有类返回类型的函数,当表达式是具有与函数返回类型相同的 cv 非限定类型的非易失性自动对象的名称时 [...] 当满足复制操作的省略标准并且要复制的对象由左值指定,首先执行为复制选择构造函数的重载决策,就好像对象由右值指定一样。

只是想再补充一点,按值返回应该是这里的默认选择,因为在最坏的情况下,return 语句中的命名值,即在 C++11、C++14 和 C++17 中没有省略被处理作为右值。例如,以下函数使用 -fno-elide-constructors 标志编译

std::unique_ptr<int> get_unique() {
  auto ptr = std::unique_ptr<int>{new int{2}}; // <- 1
  return ptr; // <- 2, moved into the to be returned unique_ptr
}

...

auto int_uptr = get_unique(); // <- 3

在编译时设置了标志,此函数中发生了两次移动(1 和 2),然后在(3)中发生了一次移动。


@juanchopanza你的意思是foo()确实也即将被销毁(如果它没有被分配给任何东西),就像函数中的返回值一样,因此C++在做{时使用移动构造函数是有道理的2}?
这个答案说允许实现做某事......它没有说它必须,所以如果这是唯一相关的部分,那意味着依赖这种行为是不可移植的。但我认为这是不对的。我倾向于认为正确的答案更多地与移动构造函数有关,如 Nikola Smiljanic 和 Bartosz Milewski 的回答中所述。
@DonHatch 它说在这些情况下“允许”执行复制/移动省略,但我们在这里不是在谈论复制省略。这是此处适用的第二个引用段落,它搭载了复制省略规则,但不是复制省略本身。第二段没有不确定性——它是完全可移植的。
@juanchopanza 我意识到这是 2 年后的事了,但你仍然觉得这是错的吗?正如我在之前的评论中提到的,这与复制省略无关。碰巧在可能应用复制省略的情况下(即使它不能与 std::unique_ptr 一起应用),有一个特殊规则首先将对象视为右值。我认为这与尼古拉的回答完全一致。
那么,当以与本示例完全相同的方式返回它时,为什么我仍然会为我的仅移动类型(已删除的复制构造函数)收到错误“尝试引用已删除的函数”?
A
Azeem

这绝不是 std::unique_ptr 特有的,而是适用于任何可移动的类。由于您按值返回,因此语言规则保证了这一点。编译器尝试删除副本,如果无法删除副本则调用移动构造函数,如果无法移动则调用复制构造函数,如果无法复制则无法编译。

如果您有一个接受 std::unique_ptr 作为参数的函数,您将无法将 p 传递给它。您必须显式调用移动构造函数,但在这种情况下,您不应在调用 bar() 后使用变量 p。

void bar(std::unique_ptr<int> p)
{
    // ...
}

int main()
{
    unique_ptr<int> p = foo();
    bar(p); // error, can't implicitly invoke move constructor on lvalue
    bar(std::move(p)); // OK but don't use p afterwards
    return 0;
}

@Fred - 好吧,不是真的。虽然 p 不是临时的,但 foo() 的结果,即返回的内容是;因此它是一个右值并且可以移动,这使得 main 中的赋值成为可能。我会说你错了,只是 Nikola 似乎将此规则应用于 p 本身,这是错误的。
正是我想说的,但找不到词。我已经删除了答案的那部分,因为它不是很清楚。
我有一个问题:在最初的问题中,第 1 行和第 2 行之间有什么实质性区别吗?在我看来是一样的,因为在 main 中构造 p 时,它只关心 foo 的返回类型,对吧?
@HongxuChen 在该示例中绝对没有区别,请参阅已接受答案中标准的引用。
实际上,你可以在之后使用 p,只要你分配给它。在此之前,您不能尝试引用内容。
P
Paul de Vrieze

unique_ptr 没有传统的复制构造函数。相反,它有一个使用右值引用的“移动构造函数”:

unique_ptr::unique_ptr(unique_ptr && src);

右值引用(双与号)只会绑定到右值。这就是为什么当您尝试将左值 unique_ptr 传递给函数时会出现错误的原因。另一方面,从函数返回的值被视为右值,因此会自动调用移动构造函数。

顺便说一句,这将正常工作:

bar(unique_ptr<int>(new int(44));

这里的临时 unique_ptr 是一个右值。


我认为重点更多,为什么 p - “显然”是一个 lvalue - 在{ 3}。我认为函数本身的返回值可以“移动”这一事实没有任何问题。
将函数的返回值包装在 std::move 中是否意味着它将被移动两次?
@RodrigoSalazar std::move 只是从左值引用 (&) 到右值引用 (&&) 的花哨转换。在右值引用上额外使用 std::move 将只是一个 noop
P
Praetorian

我认为 Scott Meyers 的 Effective Modern C++item 25 完美地解释了这一点。这是一段摘录:

标准支持 RVO 的部分继续说,如果满足 RVO 的条件,但编译器选择不执行复制省略,则返回的对象必须被视为右值。实际上,标准要求当 RVO 被允许时,要么发生复制省略,要么将 std::move 隐式应用于返回的本地对象。

这里的RVO是指返回值优化如果满足RVO的条件是指返回你在函数内部声明的本地对象会期望做 RVO,这在他的书的第 25 项中也通过引用标准很好地解释了(这里 local object 包括由返回创建的临时对象陈述)。摘录中最大的收获是发生复制省略或 std::move 隐式应用于返回的本地对象。 Scott 在第 25 项中提到,当编译器选择不删除副本并且程序员不应该显式这样做时,会隐式应用 std::move

在您的情况下,代码显然是 RVO 的候选者,因为它返回本地对象 p 并且 p 的类型与返回类型相同,这会导致复制省略。如果编译器选择不删除副本,无论出于何种原因,std::move 都会进入第 1 行。


v
v010dya

我在其他答案中没有看到的一件事是为了澄清 another answers,返回已在函数中创建的 std::unique_ptr 与已提供给那个功能。

这个例子可能是这样的:

class Test
{int i;};
std::unique_ptr<Test> foo1()
{
    std::unique_ptr<Test> res(new Test);
    return res;
}
std::unique_ptr<Test> foo2(std::unique_ptr<Test>&& t)
{
    // return t;  // this will produce an error!
    return std::move(t);
}

//...
auto test1=foo1();
auto test2=foo2(std::unique_ptr<Test>(new Test));

它在 answer by fredoverflow 中提到 - 清楚地突出显示“自动对象”。引用(包括右值引用)不是自动对象。
@TobySpeight 好的,对不起。我想我的代码只是一个澄清。
感谢您的回答!几天来我一直在尝试调试由此引起的问题,阅读这个答案让我意识到出了什么问题。
V
Vicky Gupta

我想提一个必须使用 std::move() 的情况,否则会出错。案例:如果函数的返回类型与局部变量的类型不同。

class Base { ... };
class Derived : public Base { ... };
...
std::unique_ptr<Base> Foo() {
     std::unique_ptr<Derived> derived(new Derived());
     return std::move(derived); //std::move() must
}

参考:https://www.chromium.org/developers/smart-pointer-guidelines


但是如果我删除std::move,编译时不会出错。如果复制构造函数不可调用,编译器将尝试调用移动构造函数。所以这里的 std::move 可能是多余的。