复杂返回类型是否能依赖于命名返回值优化?

5
考虑以下内容:

比如说这个:

typedef std::unordered_multiset<int> Set;
typedef std::set<Set> SetOfSets;

SetOfSets somethingRecursive(SomeType somethingToAnalyze) {
    Set s;
    // ...
    // check base cases, reduce somethingToAnalyze, fill in s
    // ...
    SetOfSets ss = somethingRecursive(somethingToAnalyze);
    ss.insert(s);
    return ss;
}

这种方法在生成子集、排列等问题上非常标准。然而,考虑到类型的内部数据结构相当复杂(std::unordered_multiset是一个哈希表,std::set通常是二叉搜索树),我尝试绘制了一个图表来解释返回值优化应该如何进行优化,但是,我只能希望编译器比我更聪明。
因此,在考虑性能(如果有必要的话)和 C++14 的情况下,我可以在这里返回一个SetOfSets吗?还是应该将其作为输出参数通过引用传递?

1
@Rakete1111 NRVO是RVO的一个子集。它有一个特殊的名称,主要是因为各种编译器首先只为rvalues实现了RVO,然后在2003年左右添加NRVO是一件大事。 - aschepler
2
我需要查看C++17的要求才能给出完整的答案。但是经典的NRVO并不关心类型有多么“复杂”。一个典型的编译器只会简单地认为“唯一的返回语句是return ss;,而且ss是一个本地非静态类类型变量,所以我将在第一时间使用调用者提供的返回地址来代替我的堆栈空间中的ss。” - aschepler
我认为这需要再进行一次开放-关闭周期。NRVO对编译器来说更加棘手,需要更高的实现能力。 - Bathsheba
2
@sigil:绝对不行。在C++17之前没有任何优化是有保证的。虽然你可以通过调试器一步一步地运行程序或者在构造函数中加入特殊的消息来查看你特定编译器的行为。 - Bathsheba
1
@YSC 因为从技术上讲,这不是复制省略。在 C++17 中,当返回 prvalue 时,没有临时材料化。只有效果“类似于应用了 RVO,或多或少”。请注意,这会产生后果 - 即使不存在复制或移动构造函数(std::atomic),也可以在 C++17 中返回 prvalue。 - Daniel Langr
显示剩余9条评论
3个回答

7
在C++17之前,你不能完全依赖复制省略,因为它是可选的。但是,所有主流编译器很可能会应用它(例如,GCC即使使用-O0优化标志也会应用它,如果想要明确禁用复制省略,则需要使用-fno-elide-constructors)。
然而,std::set支持移动语义,所以即使没有NRVO,你的代码也可以正常工作。
请注意,在C++17中,NRVO也是可选的。RVO是强制性的。
从技术上讲,我认为在C++17中没有RVO,因为当prvalue被返回时,不需要生成临时对象来进行移动/复制。规则有点不同,但效果差不多。或者更加强烈的是,在C++17中不需要复制/移动构造函数按值返回prvalue:
#include <atomic>

std::atomic<int> f() {
  return std::atomic<int>{0};
}

int main() {
  std::atomic<int> i = f();
}

在C++14中,这段代码无法编译。


根据http://en.cppreference.com/w/cpp/language/copy_elision,这是可以实现的。但是我还没有检查标准,因为我没有访问官方(非草案)版本的权限。在最新的草案中,请参见15.8.3(1.1)。 - Daniel Langr
2
@Bathsheba 在一般情况下无法保证 NRVO,因为在对象构造时无法确定该对象是否将用作返回值。 - user743382
2
我不是语言专家,所以从技术上讲,在C++17中我们可能不应该谈论RVO。Cppreference指出:C++17核心语言规范的prvalues和临时值与之前的C++修订版有根本区别:不再有要复制/移动的临时对象。描述C++17机制的另一种方式是“未实现值传递”:prvalues在不实现临时变量的情况下返回并被使用。我在这里使用这个术语来表示其效果。(请注意,C++11标准中也没有提到RVO。) - Daniel Langr
@hvd 如果我没记错的话,早期的标准已经有了限制,即您不能有多个返回不同对象的返回语句。问题是无论如何NRVO是否在这种情况下得到保证,这是一个非常简单的创建-初始化-返回模式。 - Arne Vogel

1
所以,谈到性能和(如果有必要)C++14,我是否可以在这里返回一个SetOfSets,还是应该将其作为输出参数通过引用传递? 如果您使用一个不错的编译器,您可以安全地按值返回它,因为会发生复制省略。但是,复制省略并不保证1,你的编译器可能只在给定正确的优化标志时才这样做。 当满足某些条件时,实现允许省略类对象的复制/移动构造,即使为复制/移动操作选择的构造函数和/或对象的析构函数具有副作用。 这意味着,如果未执行复制省略,您的程序仍然有意义,那么您只应依赖于复制省略。

1) 除了 constexpr 对象之外


0

不完全是这样。虽然不是保证,但这很常见,所以一切都取决于您所需的置信水平。

据我所知,此描述是最新的,并涵盖了您的主题copy_elision

关键句子是:“在以下情况下,即使复制/移动构造函数和析构函数具有可观察的副作用,编译器也被允许但不是必须省略类对象的复制和移动构造。

然后是 NRVO 的描述(您提供)。接下来的一个项目中描述了匿名临时 RVO,并且有一个项目指出自 C++17 以来这是强制性的。否则没有任何保证。

无论类型有多么复杂,“复杂”意味着什么都不重要。这些优化是基于变量的状态而发生的,例如:“一个无名临时变量,未绑定到任何引用,将被复制或移动到相同类型的对象中(忽略顶层cv限定符)”。请注意,这与变量类型的内部工作无关,而是与其使用的上下文相关。
您可以做的是创建一个最小示例并将其通过编译器进行分析汇编以查看是否发生了优化,或生成优化报告,并自行查看。

唯一的参考是明确声明允许对具有构造和析构副作用类型进行优化。但这只是一个特殊的启用条款。对于没有副作用的类型,这些已经默认情况下是可能的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接