为什么这段代码可能禁用移动语义和复制省略?

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

Why may this code disable move semantics and copy elision?

问题

有时候我们可能会像这样延迟完美的返回:

template<typename Func, typename... Args>
decltype(auto) call(Func f, Args&&... args)
{
    decltype(auto) ret{f(std::forward<Args>(args)...)};
    // ...
    return static_cast<decltype(ret)>(ret);
}

但在Jousttis的新书《C++ Move Semantics - The Complete Guide》中,他说下面的代码更好:

template<typename Func, typename... Args>
decltype(auto) call(Func f, Args&&... args)
{
    decltype(auto) ret{f(std::forward<Args>(args)...)};
    // ...
    if constexpr (std::is_rvalue_reference_v<decltype(ret)>) {
        return std::move(ret); // 将由f()返回的xvalue移动到调用者
    }
    else {
        return ret; // 返回普通值或左值引用
    }
}

因为第一段代码"可能禁用移动语义和复制省略。对于普通值,这就像在返回语句中多余地使用std::move()"。这两种模式之间有什么区别?在我看来,对于普通值,decltype将仅推断类型本身,因此它只是static_cast<Type>(ret)(即根本没有操作),返回类型与声明的类型相同,因此复制省略是可能的。我是否有任何错误理解?

英文:

Sometimes we may defer perfect returning like this:

template&lt;typename Func, typename... Args&gt;
decltype(auto) call(Func f, Args&amp;&amp;... args)
{
    decltype(auto) ret{f(std::forward&lt;Args&gt;(args)...)};
    // ...
    return static_cast&lt;decltype(ret)&gt;(ret);
}

But in Jousttis's new book C++ Move Semantics - The Complete Guide, he says that code below is better:

template&lt;typename Func, typename... Args&gt;
decltype(auto) call(Func f, Args&amp;&amp;... args)
{
    decltype(auto) ret{f(std::forward&lt;Args&gt;(args)...)};
    // ...
    if constexpr (std::is_rvalue_reference_v&lt;decltype(ret)&gt;) {
        return std::move(ret); // move xvalue returned by f() to the caller
    }
    else {
        return ret; // return the plain value or the lvalue reference
    }
}

Because the first piece of code "might disable move semantics and copy elision. For plain
values, it is like having an unnecessary std::move() in the return statement." What's the difference between these two patterns? From my point of view, for plain values, decltype will deduce just the type itself, so it's just a static_cast&lt;Type&gt;(ret)(i.e. no operation at all) and the returned type is same as the declared type so that copy elision is possible. Is there anything that I take wrong?

答案1

得分: 4

我不知道你有什么版本的书,但我的书明确说明:

完美的返回,但是不必要的复制

然而,对于引用,问题并不会出现,但当涉及按值返回时,问题就会出现。考虑以下代码:

#include <iostream>

struct S
{
    S() { std::cout << "constr\n";}
    S(const S& ) { std::cout << "copy constr\n"; }
    S(S&& ) { std::cout << "move constr\n"; }
};

S createSNoElide()
{   
    S s;
    return static_cast<decltype(s)>(s);
}

S createSElide()
{   
    S s;
    return s;
}

int main(int, char*[])
{
    std::cout << "Elision\n";
    S s1 = createSElide();
    std::cout << "No elision\n";
    S s2 = createSNoElide();
}

createSNoElide() 将被强制使用复制构造函数。根据标准,这很可能是由于以下部分:

这种复制/移动操作的省略,称为复制省略,允许在以下情况下(可以组合以消除多个副本):

在具有类返回类型的函数中的返回语句中,当表达式是具有自动存储期的非易失性对象的名称(除了函数参数或异常处理程序的异常声明引入的变量)并且具有与函数返回类型相同类型(忽略cv-资格),可以通过将对象直接构造到函数调用的返回对象来省略复制/移动操作。

即,唯一发生省略的方式是返回本地变量的名称。类型转换只是一种不同类型的表达式,它有效地阻止了省略,尽管这可能不太直观。

另外,我要额外感谢这篇文章:https://stackoverflow.com/a/55491382/4885321,它引导我到达了标准中的确切位置。

英文:

I don't know what edition of the book you have, but mine states explicitly:
> perfect return but unnecessary copy

The problem does not manifest for references, however it does when returning by value comes into play.
Consider the following code:

#include &lt;iostream&gt;

struct S
{
    S() { std::cout &lt;&lt; &quot;constr\n&quot;;}
    S(const S&amp; ) { std::cout &lt;&lt; &quot;copy constr\n&quot;; }
    S(S&amp;&amp; ) { std::cout &lt;&lt; &quot;move constr\n&quot;; }
};

S createSNoElide()
{   
    S s;
    return static_cast&lt;decltype(s)&gt;(s);
}

S createSElide()
{   
    S s;
    return s;
}

int main(int, char*[])
{
    std::cout &lt;&lt; &quot;Elision\n&quot;;
    S s1 = createSElide();
    std::cout &lt;&lt; &quot;No elision\n&quot;;
    S s2 = createSNoElide();
}

https://godbolt.org/z/YqG54rM9E

The createSNoElide() will be forced to use a copy constructor. Standard-wise it is most likely due to the following part:

> This elision of copy/move operations, called copy elision, is
> permitted in the following circumstances (which may be combined to
> eliminate multiple copies):
>
> in a return statement in a function with a class return type, when the
> expression is the name of a non-volatile object with automatic storage
> duration (other than a function parameter or a variable introduced by
> the exception-declaration of a handler ([except.handle])) with the
> same type (ignoring cv-qualification) as the function return type, the
> copy/move operation can be omitted by constructing the object directly
> into the function call's return object
>
https://eel.is/c++draft/class.copy.elision

I.e. the only way for elision to occur is to return a name of a local variable. Cast is simply a different type of expression, which effectively prevents elision, however counter-intuitive that might be.

Also, I should give extra credit to that post: https://stackoverflow.com/a/55491382/4885321 which guided me to the exact place in the standard.

huangapple
  • 本文由 发表于 2023年2月8日 22:45:48
  • 转载请务必保留本文链接:https://go.coder-hub.com/75387442.html
匿名

发表评论

匿名网友

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

确定