构建元组时的移动规则:返回时

huangapple go评论80阅读模式
英文:

Rules for move when constructing a tuple by return

问题

我有以下的C++示例(godbolt),在函数foobar中构造了一个MyStruct元组:

#include <iostream>
#include <tuple>

struct MyStruct {
    MyStruct() = default;
    MyStruct(const MyStruct&) {
        std::cout << "Copy constructor called" << std::endl;
        
    }
    MyStruct(MyStruct&&) noexcept  {
        std::cout << "Move constructor called" << std::endl;
    }
    ~MyStruct() = default;

    MyStruct& operator=(const MyStruct&) = default;
    MyStruct& operator=(MyStruct&&) noexcept = default;
};

std::tuple<MyStruct, MyStruct> foo() {
    return {MyStruct{}, MyStruct{}};
}

std::tuple<MyStruct, MyStruct> bar() {
    return {{}, {}};
}

int main() {
    std::cout << "Foo" << std::endl;
    auto [a, b] = foo();

    std::cout << "Bar" << std::endl;
    auto [c, d] = bar();
    return 0;
}

并且产生以下输出:

Foo
Move constructor called
Move constructor called
Bar
Copy constructor called
Copy constructor called

当我将这段代码放入c++ insights时,它为foobar都创建了相同的函数。所以,我理解的是foobar都应该移动对象,而不是复制它。有没有人知道为什么行为不同?

这个问题类似,但不同之处在于我想知道为什么bar会复制值而不是移动它。

英文:

I have the following c++ example (godbolt) that constructs a tuple of MyStruct in the functions foo and bar:

#include &lt;iostream&gt;
#include &lt;tuple&gt;

struct MyStruct {
    MyStruct() = default;
    MyStruct(const MyStruct&amp;) {
        std::cout &lt;&lt; &quot;Copy constructor called&quot; &lt;&lt; std::endl;
        
    }
    MyStruct(MyStruct&amp;&amp;) noexcept  {
        std::cout &lt;&lt; &quot;Move constructor called&quot; &lt;&lt; std::endl;
    }
    ~MyStruct() = default;

    MyStruct&amp; operator=(const MyStruct&amp;) = default;
    MyStruct&amp; operator=(MyStruct&amp;&amp;) noexcept = default;
};

std::tuple&lt;MyStruct, MyStruct&gt; foo() {
    return {MyStruct{}, MyStruct{}};
}

std::tuple&lt;MyStruct, MyStruct&gt; bar() {
    return {{}, {}};
}

int main() {
    std::cout &lt;&lt; &quot;Foo&quot; &lt;&lt; std::endl;
    auto [a, b] = foo();

    std::cout &lt;&lt; &quot;Bar&quot; &lt;&lt; std::endl;
    auto [c, d] = bar();
    return 0;
}

And produces the following output:

Foo
Move constructor called
Move constructor called
Bar
Copy constructor called
Copy constructor called

When I put this code in c++ insights, it creates the same function for both foo and bar. So, my understanding is that both foo and bar should move the object instead of bar copying it. Does anyone know why the behaviour is different?

This question is similar but it's not the same as I'm wondering why bar copies the value instead of moving it.

答案1

得分: 3

cppinsights.io在这里是错误的,它不会生成与您的原始代码具有相同语义的代码。
return {{}, {}} 调用复制构造函数的原因是 std::tuple 的奇怪构造函数和重载分辨率的组合。这里有两个重要的构造函数:

tuple(const Types&... args); // (2)

template< class... UTypes >
tuple(UTypes&&... args); // (3)

在您的两个返回语句中,您都返回 prvalues,这使得 (3) 更匹配,因为转换序列更短(没有添加 const)。
如果可能的话,这个完美转发的重载将被选择,并调用移动构造函数。

然而,对于 {{}, {}} 来说,这是不可能的,因为Utypes 包中的类型无法从 {} 推断出来。一般来说,您只能在类型可以被推断的上下文中使用这些大括号表达式。
例如:

void take_int(int);
void take_any(auto);

int main() {
    take_int({}); // OK,int的值初始化
    take_any({}); // 非法,无法从 {} 推断类型
}

因此,{{}, {}} 将使用第一个构造函数,这涉及到复制构造。
我们可以像这样重现这个问题:

template <typename ...Ts>
struct tuple {
    tuple(const Ts&...);

    template <typename ...Us>
    tuple(Us&&...);
};

struct MyStruct {
    MyStruct() = default;
    MyStruct(const MyStruct&);
    MyStruct(MyStruct&&) noexcept;
};

tuple<MyStruct, MyStruct> foo() {
    return {MyStruct{}, MyStruct{}};
}

tuple<MyStruct, MyStruct> bar() {
    return {{}, {}};
}

这段代码编译为:

foo():
    # ...
    call tuple<MyStruct, MyStruct>::tuple<MyStruct, MyStruct>(MyStruct&&, MyStruct&&)@PLT
    # ...
    ret
bar():
    # ...
    call tuple<MyStruct, MyStruct>::tuple(MyStruct const&, MyStruct const&)@PLT
    # ...
    ret

Compiler Explorer 上查看实时示例

英文:

cppinsights.io is wrong here, and doesn't produce code that has the same semantics as your original.
The reason why return {{}, {}} calls the copy constructor is a combination of std::tuple's weird constructors, and overload resolution. There are two important constructors here:

tuple( const Types&amp;... args ); // (2)

template&lt; class... UTypes &gt;
tuple( UTypes&amp;&amp;... args ); // (3)

In both of your return-statements, you are returning prvalues, making (3) a better match because the conversion sequence is shorter (no const added).
If possible, this perfect forwarding overload is going to be chosen, and the move constructor is called.

However, this is not possible for {{}, {}}, because the types in the Utypes pack cannot be inferred from {}. In general, you can only use these brace expressions in a context where the type can be inferred.
For example:

void take_int(int);
void take_any(auto);

int main() {
    take_int({}); // OK, value initialization of an int
    take_any({}); // ill-formed, cannot infer type from {}
}

As a consequence, {{}, {}} will use the first constructor, which involves copy construction.
We can reproduce this issue like this:

template &lt;typename ...Ts&gt;
struct tuple {
    tuple(const Ts&amp;...);

    template &lt;typename ...Us&gt;
    tuple(Us&amp;&amp;...);
};

struct MyStruct {
    MyStruct() = default;
    MyStruct(const MyStruct&amp;);
    MyStruct(MyStruct&amp;&amp;) noexcept;
};

tuple&lt;MyStruct, MyStruct&gt; foo() {
    return {MyStruct{}, MyStruct{}};
}

tuple&lt;MyStruct, MyStruct&gt; bar() {
    return {{}, {}};
}

This code compiles to:

foo():
    # ...
    call tuple&lt;MyStruct, MyStruct&gt;::tuple&lt;MyStruct, MyStruct&gt;(MyStruct&amp;&amp;, MyStruct&amp;&amp;)@PLT
    # ...
    ret
bar():
    # ...
    call tuple&lt;MyStruct, MyStruct&gt;::tuple(MyStruct const&amp;, MyStruct const&amp;)@PLT
    # ...
    ret

See live example on Compiler Explorer

huangapple
  • 本文由 发表于 2023年6月19日 21:02:38
  • 转载请务必保留本文链接:https://go.coder-hub.com/76506917.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定