为什么命名返回值优化在这里不起作用?

Why isn't named return value optimization working here?

提问人:Zebrafish 提问时间:10/5/2023 最后编辑:Zebrafish 更新时间:10/5/2023 访问量:110

问:

我知道我已经删除了复制构造函数,我假设这没问题,因为我期待命名返回值优化并且会发生直接初始化。是否需要声明复制构造函数?如果我这样做了,我就会遇到一个问题,即班级的成员也无法被复制,那么我该怎么办?

class UniqueBufferPointer
{public:
    UniqueBufferPointer() {}
    UniqueBufferPointer(const UniqueBufferPointer& other) = delete;
    UniqueBufferPointer(UniqueBufferPointer&& other) {}
    UniqueBufferPointer& operator=(const UniqueBufferPointer&) = delete;

};


struct GFXAPIImage
{
    GFXAPIImage() {}

    UniqueBufferPointer handle;
    GFXAPIImage(const GFXAPIImage&) = delete;
    //GFXAPIImage(const GFXAPIImage& other) = default; // If I use this instead of the compiler complaining that it can't access the copy constructor of this class, it complains it can't access the copy constructor of UniqueBufferPointer.

};


GFXAPIImage func()
{
    GFXAPIImage f;
    return f; //GFXAPIImage(const GFXAPIImage&) cannot be referenced, it is a deleted function
}


int main()
{

    GFXAPIImage f = func();
}

我在Visual STudio上并使用/O2优化标志进行编译。

编辑:此外,如果我为GFXAPIImage声明一个移动构造函数,它就会编译。这是否意味着它使用 move 构造函数?为什么?这是一个 l 值,它不应该有业务移动它。

C++ 返回值优化

评论

4赞 YSC 10/5/2023
无关:那个位置很糟糕。public:
0赞 YSC 10/5/2023
你使用的是哪个版本的 Visual Studio?您是定义特定的 C++ 标准还是使用默认值?
0赞 Zebrafish 10/5/2023
@YSC 版本 17.2.3 和 std:c++latest
0赞 Paul Sanders 10/5/2023
这是一个 l 值,它不应该有业务移动它。我相信NRVO是这条规则的例外。

答:

5赞 YSC 10/5/2023 #1

命名返回值优化是一种非强制性的复制省略。这意味着返回的对象必须具有可访问的、未删除的复制或移动构造函数。

引用 cppreference.com

非强制复制/移动(自 C++11 起)省略

在以下情况下,编译器是允许的,但不是必需的,即使复制/移动(自 C++11 起)构造函数和析构函数具有可观察到的副作用,编译器也不需要省略类对象的复制和移动(自 C++11 起)构造。这些对象被直接构造到存储中,否则它们将被复制/移动到该存储中。这是一个优化:即使它发生了并且复制/移动(因为 C++11)构造函数没有被调用,它仍然必须存在和可访问(好像根本没有发生优化),否则程序格式不正确

  • 在 return 语句中,当操作数是具有自动存储持续时间的非易失性对象的名称时,该对象不是函数参数或 catch 子句参数,并且与函数返回类型具有相同的类类型(忽略 cv-qualification)。这种复制省略的变体称为 NRVO,即“命名返回值优化”。

引用标准 [class.copy.elision]/1

当满足某些条件时,允许实现省略复制/移动构造 [...]

  • 在具有类返回类型的函数的 RETURN 语句中,当表达式是具有自动存储持续时间的非易失性对象的名称时 [...]
2赞 Brian61354270 10/5/2023 #2

通过显式删除 的 copy 构造函数,可以防止编译器提供隐式定义的默认移动构造函数。GFXAPIImage

这是一个问题,因为当你写 ,对象需要被复制或移动。但是如果没有移动构造函数,就无法移动它,并且删除了复制构造函数,则无法复制它。Boom:编译器错误。return ff

请注意,NVRO在这里实际上并不相关。NVRO 的含义是,当从函数返回命名对象时,允许编译器选择性地省略对的调用。但这只是一个优化。你仍然需要首先存在,代码才能形成良好的格式。GFXAPIImage::GFXAPIImage(GFXAPIImage&&)GFXAPIImage::GFXAPIImage(GFXAPIImage&&)

要修复代码,只需从 中删除显式删除的复制构造函数即可。这将使编译器能够提供隐式定义的默认移动构造函数,这将使移动再次可移动。或者,您可以改为定义自己的移动构造函数,或者请求编译器提供默认的移动构造函数,即使删除了带有 .GFXAPIImageGFXAPIImage= default

请注意,即使没有显式删除复制构造函数,它仍会被隐式删除。因此,您不会因省略显式删除而失去任何安全性。

另请参阅自动生成默认/复制/移动 ctor 和复制/移动赋值运算符的条件?

struct GFXAPIImage
{
    GFXAPIImage() {}

    UniqueBufferPointer handle;
    // no explicitly deleted copy constructor

};


GFXAPIImage func()
{
    GFXAPIImage f;
    return f; //  OKAY, calls move constructor
}

int main()
{

    GFXAPIImage f = func();
    // GFXAPIImage g = f; // would fail due to GFXAPIImage(const GFXAPIImage&) being implicitly deleted
}

评论

0赞 Zebrafish 10/5/2023
我刚刚意识到添加一个显式移动构造函数并默认它使它编译。为什么有必要这样做?它是否调用了移动构造函数?
1赞 Brian61354270 10/5/2023
@Zebrafish 这是一项“安全功能”。如果你的类定义了一个复制构造函数,那么默认的移动构造函数很有可能会破坏它。考虑一个仅定义复制构造函数的自定义项。如果它有一个隐式定义的默认移动构造函数,它只是复制了存储指针,就会发生不好的事情。std::vector
0赞 Brian61354270 10/5/2023
@Zebrafish我通过回答进行了编辑,以进一步阐明为什么这里需要移动构造函数