有些类型很难复制,例如:
struct S
{
int a[100];
};
任务是处理这种类型的变量的值并返回修改后的副本,即 必须保留原件。建议两种方法:
S test(const S& s) { S news = s; // делаем копию news.a[42] = 100500; // изменяем return news; // возвращаем }S test(S s) // делаем копию { s.a[42] = 100500; // изменяем return s; // возвращаем }
选项 2 在代码方面看起来更短,但是,如汇编所示,按引用传递会导致汇编代码更短。
为什么会发生这种情况?这两种方法的优缺点是什么,以便了解更喜欢哪一种?也许还有其他选择?
在现代 C++ 中,第二个选项被认为更可取。那些。如果你知道无论如何你都需要一个副本,最好让编译器为你制作那个副本,而不是你自己。
然而,这个断言的传统理由依赖于“难以”复制的类型,不是因为它们本身很大,而是因为它们需要深度复制。那些。我们谈论的是在浅(浅)级别紧凑但通过指针/句柄拥有额外资源的类型。这里的整个想法是,在原始值是临时/可重定位的情况下,编译器将能够用移动替换复制。
例如,如果在您的情况下将其替换
S为std::string,则在调用test("abc")第二个选项时,在准备参数时,它将完全不进行深度复制,而在第一个选项中,您自己将无条件地执行深度复制。const std::string &(带有参数和参数的两个单独函数的选项可能更有效std::string &&,但如果您不想压缩最后的处理器周期,那么带有参数的单个函数std::string通常看起来更有吸引力。)在对象的“重量”直接构建到对象本身的情况下,如您的示例中,将无法节省复制。我希望这两个选项具有相同的性能。
一些细微差别是,根据语言的抽象语义,在第二个变体中创建副本是在调用代码的上下文中完成的。一些实现从字面上遵循这种语义——它们在调用代码的上下文中为其执行副本创建和内存分配。在这种情况下,可以提前保留内存,而不管函数在执行期间是否会被实际调用。那些。比如说,写这样一个递归函数
您可能会惊讶地发现,在使用函数的第二个版本时,在每个递归级别都分配了
test用于复制的堆栈空间,而实际上只有在递归的最底部才需要此内存。第一个选项将没有这个缺点。stest其他实现可能更经济:即使使用第二个选项
test,仅在实际调用函数时才保留内存。引用 McConnell 的完美代码,第 7.5 章:
恕我直言:这是错误的余地。虽然他自己写道:
...并且不会感到内疚:)
根据 McConnell 的推理,通过引用传递参数并制作本地副本更有意义。
第一个选项显然更好,因为它符合多年来推广的良好 C++ 代码的概念。这样的代码不会引起任何问题,但不是通过引用传递的结构会引起问题。而且结构的规模越大,这样的决定就会引发更多的问题。如果它提出问题,那么它需要评论。另外,正如您自己在问题中指出的那样,您没有衡量这两种方法的有效性(汇编程序列表的大小根本没有任何意义),因此没有什么可参考的。
在我看来,这个问题是尝试过早优化的典型示例,通常会导致悲观情绪,对代码可读性产生负面影响。
关于这个主题写了很长的文字。可以在此链接中找到:通过引用传递还是按值传递?
在您链接到的 x86-64 clang 汇编器中,第一个和第二个选项之间的区别是:
在第一个变体中,优化被触发,结构从作为参数传递的地址立即复制到返回值的位置。退货不需要任何额外的步骤。
在第二个变体中,没有应用这种优化,结构被复制了两次。第一次调用代码创建一个副本以传递给函数(在给定代码的括号之外),第二次从函数返回时。
第二个选项由于 operator 的原因而长了一行,它只
lea rsi, [rsp + 16]计算memcpy调用的源参数(在第一种情况下,这不是必需的,因为它已显式传递给调用函数)。因此,由于结构的双重复制和memcpy的参数的额外计算,第二个选项在真空中呈球形,效率不高。我想在真正的程序中你不必担心这个。优化编译器不会考虑单个函数,而是将程序作为一个整体来考虑,并会选择最佳选项。尽管第二个选项理论上“更糟”,但实际上编译器只会将两个函数都变成内联函数(如果它们不带指针),并且不会有任何区别。
作为一个合适的选择