为什么可以在没有 std::move 的情况下返回 std::unique_ptr ?

发布于 2024-10-05 09:36:38 字数 722 浏览 10 评论 0原文

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

#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 这个异常,因为返回值是一个临时对象,一旦函数退出就会被销毁,从而保证返回指针的唯一性。我很好奇这是如何实现的,编译器中是否有特殊情况,或者语言规范中是否有其他条款可以利用?

unique_ptr<T> does not allow copy construction, instead it supports move semantics. Yet, I can return a unique_ptr<T> from a function and assign the returned value to a variable.

#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;
}

The code above compiles and works as intended. So how is it that line 1 doesn't invoke the copy constructor and result in compiler errors? If I had to use line 2 instead it'd make sense (using line 2 works as well, but we're not required to do so).

I know C++0x allows this exception to unique_ptr since the return value is a temporary object that will be destroyed as soon as the function exits, thus guaranteeing the uniqueness of the returned pointer. I'm curious about how this is implemented, is it special cased in the compiler or is there some other clause in the language specification that this exploits?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(7

美男兮 2024-10-12 09:36:38

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

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

当满足某些条件时,允许实现省略类对象的复制/移动构造[...]
这种复制/移动操作的省略称为复制省略,是允许的 [...]
在具有类返回类型的函数的 return 语句中,当表达式是
一个非易失性自动对象
,具有与函数返回类型相同的 cv 非限定类型 [...]

当满足复制操作省略的条件并且要复制的对象由左值指定时,
首先执行重载解析来选择副本的构造函数就像对象是由右值指定的


只是想再补充一点,按值返回应该是这里的默认选择,因为在最坏的情况下,即在 C++11、C++14 和 C++17 中没有省略的情况下,会处理 return 语句中的命名值作为右值。因此,例如以下函数使用 -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 )。

is there some other clause in the language specification that this exploits?

Yes, see 12.8 §34 and §35:

When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object [...]
This elision of copy/move operations, called copy elision, is permitted [...]
in a return statement in a function with a class return type, when the expression is the name of
a non-volatile automatic object
with the same cv-unqualified type as the function return type [...]

When the criteria for elision of a copy operation are met and the object to be copied is designated by an lvalue,
overload resolution to select the constructor for the copy is first performed as if the object were designated by an rvalue.


Just wanted to add one more point that returning by value should be the default choice here because a named value in the return statement in the worst case, i.e. without elisions in C++11, C++14 and C++17 is treated as an rvalue. So for example the following function compiles with the -fno-elide-constructors flag

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

With the flag set on compilation there are two moves (1 and 2) happening in this function and then one move later on (3).

感情废物 2024-10-12 09:36:38

这绝不是特定于 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;
}

This is in no way specific to std::unique_ptr, but applies to any class that is movable. It's guaranteed by the language rules since you are returning by value. The compiler tries to elide copies, invokes a move constructor if it can't remove copies, calls a copy constructor if it can't move, and fails to compile if it can't copy.

If you had a function that accepts std::unique_ptr as an argument you wouldn't be able to pass p to it. You would have to explicitly invoke move constructor, but in this case you shouldn't use variable p after the call to bar().

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;
}
祁梦 2024-10-12 09:36:38

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

unique_ptr::unique_ptr(unique_ptr && src);

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

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

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

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

unique_ptr doesn't have the traditional copy constructor. Instead it has a "move constructor" that uses rvalue references:

unique_ptr::unique_ptr(unique_ptr && src);

An rvalue reference (the double ampersand) will only bind to an rvalue. That's why you get an error when you try to pass an lvalue unique_ptr to a function. On the other hand, a value that is returned from a function is treated as an rvalue, so the move constructor is called automatically.

By the way, this will work correctly:

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

The temporary unique_ptr here is an rvalue.

清晰传感 2024-10-12 09:36:38

我认为 Scott Meyers 的Effective Modern C++<的第 25 项对此进行了完美的解释/a>.这是摘录:

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

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

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

I think it's perfectly explained in item 25 of Scott Meyers' Effective Modern C++. Here's an excerpt:

The part of the Standard blessing the RVO goes on to say that if the conditions for the RVO are met, but compilers choose not to perform copy elision, the object being returned must be treated as an rvalue. In effect, the Standard requires that when the RVO is permitted, either copy elision takes place or std::move is implicitly applied to local objects being returned.

Here, RVO refers to return value optimization, and if the conditions for the RVO are met means returning the local object declared inside the function that you would expect to do the RVO, which is also nicely explained in item 25 of his book by referring to the standard (here the local object includes the temporary objects created by the return statement). The biggest take away from the excerpt is either copy elision takes place or std::move is implicitly applied to local objects being returned. Scott mentions in item 25 that std::move is implicitly applied when the compiler choose not to elide the copy and the programmer should not explicitly do so.

In your case, the code is clearly a candidate for RVO as it returns the local object p and the type of p is the same as the return type, which results in copy elision. And if the compiler chooses not to elide the copy, for whatever reason, std::move would've kicked in to line 1.

天冷不及心凉 2024-10-12 09:36:38

我在其他答案中没有看到的一件事是为了澄清另一个答案存在差异返回已在函数内创建的 std::unique_ptr 和已赋予该函数的 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));

One thing that i didn't see in other answers is To clarify another answers that there is a difference between returning std::unique_ptr that has been created within a function, and one that has been given to that function.

The example could be like this:

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));
夜访吸血鬼 2024-10-12 09:36:38

我想提一下一种情况,您必须使用 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

I would like to mention one case where you must use std::move() otherwise it will give an error.
Case: If the return type of the function differs from the type of the local variable.

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
}

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

我知道这是一个老问题,但我认为这里缺少一个重要而明确的参考。

来自 https://en.cppreference.com/w/cpp/language/copy_elision:

(C++11 起) 在 return 语句或 throw 表达式中,如果编译器无法执行复制省略,但满足或将满足复制省略的条件(源是函数参数除外),则即使对象是由左值指定的,编译器也会尝试使用移动构造函数;详情请参阅退货声明。

I know it's an old question, but I think an important and clear reference is missing here.

From https://en.cppreference.com/w/cpp/language/copy_elision :

(Since C++11) In a return statement or a throw-expression, if the compiler cannot perform copy elision but the conditions for copy elision are met or would be met, except that the source is a function parameter, the compiler will attempt to use the move constructor even if the object is designated by an lvalue; see return statement for details.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文