禁用复制/移动省略时如何有效地返回对象?

How to efficiently return an object when copy/move elision is disabled?

提问人:chakmeshma 提问时间:10/21/2023 最后编辑:chakmeshma 更新时间:10/23/2023 访问量:194

问:

假设在编译时禁用了复制省略,那么以下操作是否是避免不必要的复制(模拟复制省略)的有效做法?

Container getContainer() {
    Container c;

    return c;
}

int main() {
    Container&& contianer = getContainer();
}
C++ 右值引用 复制省略

评论

5赞 Jarod42 10/21/2023
你说的是 NRVO,还是在?(在 C++17 中,一些“复制省略”是强制性的)。main
2赞 freakish 10/21/2023
如果禁用了复制省略,那么您可以随时手动执行此操作,例如通过传递 ref arg 返回。无论如何,编译器就是这样做的。
4赞 user17732522 10/21/2023
Container&& contianer = getContainer();顺便说一句,毫无意义。 行为完全相同,只是第一个更难理解。由于 C++17 两者都不创建副本(无论是否启用了复制省略),并且在 C++17 之前,如果未完成复制省略,它们都会创建副本。Container contianer = getContainer();
2赞 463035818_is_not_an_ai 10/21/2023
当禁用复制省略时,您创建副本,还有什么目标?
2赞 freakish 10/21/2023
@chakmeshma为什么不想依赖编译器呢?只要您遵循一些基本规则,复制省略是强制性的。

答:

0赞 digito_evo 10/22/2023 #1

我没有足够的资格讨论有关NRVO的标准的细微差别,但我们可以尝试与下面的代码片段类似地测试它(使用GCC 13.2和C++11模式加上选项。直播):-fno-elide-constructors

#include <vector>
#include <cstdio>


std::vector<int> create( const int in )
{
    std::vector<int> foo ( 5, in );
    return foo;
}

int main( int argc, char* argv[] )
{
    std::vector<int>&& result { create( argc ) }; // remove `&&` and it will result
                                                  // in slightly more assembly code

    for ( auto&& val : result )
        std::printf( "%d\n", val );
}

似乎添加到类型中消除了对一些额外和说明的需求。话虽如此,您可能需要衡量性能差异。在某些情况下,生成的机器代码可能会产生误导。&&resultmovmovdqa

另一方面,Clang 17.0.1 为两种情况生成相同的代码(有或没有)。&&

1赞 Nhat Nguyen 10/22/2023 #2

从 C++17 开始保证复制省略,除非您使用标志禁用,例如 gcc 中的 -fno-elide-constructors。

如果您禁用了复制省略,并且您仍然想要一种高效返回对象的方法(我认为高效是指低运行时开销),那么临时对象生存期延长是要走的路。可以使用常量左值引用或转发引用来延长临时引用的生存期。

移动省略是一回事吗?请记住,如果对返回值执行 std::move,则会弄乱 RVO 和 NRVO。当前的标准措辞目前仅将对象作为 RVO/NRVO 的返回值处理,std::move 将返回右值引用,这不是 C++ 中的对象。

评论

1赞 NathanOliver 10/22/2023
-fno-elide-constructors对 C++17 的保证复制省略没有影响,因为没有省略。
4赞 Brian Bi 10/22/2023 #3

从 C++17 开始,某些形式的复制省略不再是复制省略,因为该语言不再指定创建临时的。因此,编译器首先不需要省略任何东西。例如,你可以这样写:

Container container = getContainer();

并且没有为 的返回值创建临时对象。相反,程序直接将结果构造为 .因此,您不需要编写 .getContainercontainerContainer&& container = getContainer()

另一方面,NRVO 不能保证。当我们看这个函数定义时:

Container getContainer() {
    Container c;

    return c;
}

您正在创建一个名为 的局部变量,并请求对返回对象进行移动初始化。如果编译器执行 NRVO,则省略该变量。但是,如果您希望保证省略本地对象(因此不需要移动),那么这样做的方法是首先不声明本地对象。cccContainerContainer

在简单的情况下,您可以在最后一分钟创建返回值:

Container getContainer() {
    // ...
    return Container();
}

上面的代码没有创建临时对象(从 C++17 开始)。

但是,如果您需要构造返回值,对它执行某些操作,然后稍后返回,则可能需要声明函数以采用类型的 out-参数,该函数将突变为它想要返回的值。在某些情况下,调用方可能无法构造要传递到函数中的对象。在这种情况下,该函数必须采用指针参数,该值将使用放置 new 构造到该参数中。在这种情况下,调用方必须注意在作用域结束时手动调用析构函数。Container&Container