C++视图类型:按常量传递还是按值传递?
C++ view types: pass by const& or by value?
这是在最近的代码审查讨论中提出的,但没有一个令人满意的结论。所讨论的类型类似于C++ string_view TS。它们是围绕指针和长度的简单非拥有包装器,用一些自定义函数装饰:
#include <cstddef>
class foo_view {
public:
foo_view(const char* data, std::size_t len)
: _data(data)
, _len(len) {
}
// member functions related to viewing the 'foo' pointed to by '_data'.
private:
const char* _data;
std::size_t _len;
};
问题是,是否有任何一种论据来倾向于按值或常量引用传递此类视图类型(包括即将推出的string_view和array_view类型(。
支持按值传递的论点相当于"较少的类型","如果视图具有有意义的突变,则可以改变本地副本"和"可能不降低效率"。
支持通过 const 引用传递的论点相当于"通过 const 传递对象更惯用",并且"可能效率不低"。
是否有任何其他注意事项可能会以一种或另一种方式最终改变论点,即按值还是按常量引用传递惯用视图类型更好。
对于这个问题,可以安全地假设C++11或C++14语义,以及足够现代的工具链和目标架构等。
如有疑问,请按值传递。
现在,您应该很少怀疑。
通常,价值传递起来很昂贵,而且几乎没有好处。 有时,您实际上希望引用存储在其他位置的可能变异值。 通常,在通用代码中,您不知道复制是否是一项昂贵的操作,因此您犯了错误。
在有疑问时应该按值传递的原因是值更容易推理。 当您调用函数回调或您有什么时,对外部数据的引用(即使是const
引用(可能会在算法中间发生变化,从而使看似简单的函数陷入复杂的混乱。
在这种情况下,您已经有一个隐式引用绑定(到您正在查看的容器的内容(。 添加另一个隐式引用绑定(到查看容器的视图对象(同样糟糕,因为已经存在复杂性。
最后,编译器可以更好地推理值,而不是推理值的引用。 如果离开本地分析的范围(通过函数指针回调(,编译器必须假定存储在 const 引用中的值可能已完全更改(如果它不能证明相反(。 自动存储中的值,没有人接受指向它的指针,可以假定它不会以类似的方式修改 - 没有定义的方式来访问它并从外部范围更改它,因此可以假定此类修改不会发生。
当您有机会将值作为值传递时,请拥抱简单性。 这种情况很少发生。
编辑:代码可在此处获得:https://github.com/acmorrow/stringview_param
我创建了一些示例代码,这些代码似乎表明,对类似string_view对象的按值传递至少可以为一个平台上的调用方和函数定义提供更好的代码。
首先,我们在string_view.h
中定义了一个假string_view类(我手边没有真的东西(:
#pragma once
#include <string>
class string_view {
public:
string_view()
: _data(nullptr)
, _len(0) {
}
string_view(const char* data)
: _data(data)
, _len(strlen(data)) {
}
string_view(const std::string& data)
: _data(data.data())
, _len(data.length()) {
}
const char* data() const {
return _data;
}
std::size_t len() const {
return _len;
}
private:
const char* _data;
size_t _len;
};
现在,让我们定义一些按值或引用使用string_view的函数。以下是example.hpp
中的签名:
#pragma once
class string_view;
void __attribute__((visibility("default"))) use_as_value(string_view view);
void __attribute__((visibility("default"))) use_as_const_ref(const string_view& view);
这些函数的主体定义如下,example.cpp
:
#include "example.hpp"
#include <cstdio>
#include "do_something_else.hpp"
#include "string_view.hpp"
void use_as_value(string_view view) {
printf("%ld %ld %zun", strchr(view.data(), 'a') - view.data(), view.len(), strlen(view.data()));
do_something_else();
printf("%ld %ld %zun", strchr(view.data(), 'a') - view.data(), view.len(), strlen(view.data()));
}
void use_as_const_ref(const string_view& view) {
printf("%ld %ld %zun", strchr(view.data(), 'a') - view.data(), view.len(), strlen(view.data()));
do_something_else();
printf("%ld %ld %zun", strchr(view.data(), 'a') - view.data(), view.len(), strlen(view.data()));
}
这里的do_something_else
函数是对编译器无法了解的函数(例如来自其他动态对象的函数等(的任意调用的替代项。声明do_something_else.hpp
:
#pragma once
void __attribute__((visibility("default"))) do_something_else();
琐碎的定义是do_something_else.cpp
:
#include "do_something_else.hpp"
#include <cstdio>
void do_something_else() {
std::printf("Doing somethingn");
}
我们现在将do_something_else.cpp和示例.cpp编译为单独的动态库。这里的编译器是OS X Yosemite 10.10.1上的XCode 6 clang:
clang++ -mmacosx-version-min=10.10 --stdlib=libc++ -O3 -flto -march=native -fvisibility-inlines-hidden -fvisibility=hidden --std=c++11 ./do_something_else.cpp -fPIC -shared -o libdo_something_else.dylib
clang++ -mmacosx-version-min=10.10 --stdlib=libc++ -O3 -flto -march=native -fvisibility-inlines-hidden -fvisibility=hidden --std=c++11 ./example.cpp -fPIC -shared -o libexample.dylib -L. -ldo_something_else
现在,我们反汇编libexample.dylib:
> otool -tVq ./libexample.dylib
./libexample.dylib:
(__TEXT,__text) section
__Z12use_as_value11string_view:
0000000000000d80 pushq %rbp
0000000000000d81 movq %rsp, %rbp
0000000000000d84 pushq %r15
0000000000000d86 pushq %r14
0000000000000d88 pushq %r12
0000000000000d8a pushq %rbx
0000000000000d8b movq %rsi, %r14
0000000000000d8e movq %rdi, %rbx
0000000000000d91 movl $0x61, %esi
0000000000000d96 callq 0xf42 ## symbol stub for: _strchr
0000000000000d9b movq %rax, %r15
0000000000000d9e subq %rbx, %r15
0000000000000da1 movq %rbx, %rdi
0000000000000da4 callq 0xf48 ## symbol stub for: _strlen
0000000000000da9 movq %rax, %rcx
0000000000000dac leaq 0x1d5(%rip), %r12 ## literal pool for: "%ld %ld %zun"
0000000000000db3 xorl %eax, %eax
0000000000000db5 movq %r12, %rdi
0000000000000db8 movq %r15, %rsi
0000000000000dbb movq %r14, %rdx
0000000000000dbe callq 0xf3c ## symbol stub for: _printf
0000000000000dc3 callq 0xf36 ## symbol stub for: __Z17do_something_elsev
0000000000000dc8 movl $0x61, %esi
0000000000000dcd movq %rbx, %rdi
0000000000000dd0 callq 0xf42 ## symbol stub for: _strchr
0000000000000dd5 movq %rax, %r15
0000000000000dd8 subq %rbx, %r15
0000000000000ddb movq %rbx, %rdi
0000000000000dde callq 0xf48 ## symbol stub for: _strlen
0000000000000de3 movq %rax, %rcx
0000000000000de6 xorl %eax, %eax
0000000000000de8 movq %r12, %rdi
0000000000000deb movq %r15, %rsi
0000000000000dee movq %r14, %rdx
0000000000000df1 popq %rbx
0000000000000df2 popq %r12
0000000000000df4 popq %r14
0000000000000df6 popq %r15
0000000000000df8 popq %rbp
0000000000000df9 jmp 0xf3c ## symbol stub for: _printf
0000000000000dfe nop
__Z16use_as_const_refRK11string_view:
0000000000000e00 pushq %rbp
0000000000000e01 movq %rsp, %rbp
0000000000000e04 pushq %r15
0000000000000e06 pushq %r14
0000000000000e08 pushq %r13
0000000000000e0a pushq %r12
0000000000000e0c pushq %rbx
0000000000000e0d pushq %rax
0000000000000e0e movq %rdi, %r14
0000000000000e11 movq (%r14), %rbx
0000000000000e14 movl $0x61, %esi
0000000000000e19 movq %rbx, %rdi
0000000000000e1c callq 0xf42 ## symbol stub for: _strchr
0000000000000e21 movq %rax, %r15
0000000000000e24 subq %rbx, %r15
0000000000000e27 movq 0x8(%r14), %r12
0000000000000e2b movq %rbx, %rdi
0000000000000e2e callq 0xf48 ## symbol stub for: _strlen
0000000000000e33 movq %rax, %rcx
0000000000000e36 leaq 0x14b(%rip), %r13 ## literal pool for: "%ld %ld %zun"
0000000000000e3d xorl %eax, %eax
0000000000000e3f movq %r13, %rdi
0000000000000e42 movq %r15, %rsi
0000000000000e45 movq %r12, %rdx
0000000000000e48 callq 0xf3c ## symbol stub for: _printf
0000000000000e4d callq 0xf36 ## symbol stub for: __Z17do_something_elsev
0000000000000e52 movq (%r14), %rbx
0000000000000e55 movl $0x61, %esi
0000000000000e5a movq %rbx, %rdi
0000000000000e5d callq 0xf42 ## symbol stub for: _strchr
0000000000000e62 movq %rax, %r15
0000000000000e65 subq %rbx, %r15
0000000000000e68 movq 0x8(%r14), %r14
0000000000000e6c movq %rbx, %rdi
0000000000000e6f callq 0xf48 ## symbol stub for: _strlen
0000000000000e74 movq %rax, %rcx
0000000000000e77 xorl %eax, %eax
0000000000000e79 movq %r13, %rdi
0000000000000e7c movq %r15, %rsi
0000000000000e7f movq %r14, %rdx
0000000000000e82 addq $0x8, %rsp
0000000000000e86 popq %rbx
0000000000000e87 popq %r12
0000000000000e89 popq %r13
0000000000000e8b popq %r14
0000000000000e8d popq %r15
0000000000000e8f popq %rbp
0000000000000e90 jmp 0xf3c ## symbol stub for: _printf
0000000000000e95 nopw %cs:(%rax,%rax)
有趣的是,按值版本短了几条指令。但这只是功能机构。来电者呢?
我们将定义一些调用这两个重载的函数,转发一个const std::string&
,example_users.hpp
:
#pragma once
#include <string>
void __attribute__((visibility("default"))) forward_to_use_as_value(const std::string& str);
void __attribute__((visibility("default"))) forward_to_use_as_const_ref(const std::string& str);
并用example_users.cpp
定义它们:
#include "example_users.hpp"
#include "example.hpp"
#include "string_view.hpp"
void forward_to_use_as_value(const std::string& str) {
use_as_value(str);
}
void forward_to_use_as_const_ref(const std::string& str) {
use_as_const_ref(str);
}
同样,我们将example_users.cpp
编译为共享库:
clang++ -mmacosx-version-min=10.10 --stdlib=libc++ -O3 -flto -march=native -fvisibility-inlines-hidden -fvisibility=hidden --std=c++11 ./example_users.cpp -fPIC -shared -o libexample_users.dylib -L. -lexample
再一次,我们看看生成的代码:
> otool -tVq ./libexample_users.dylib
./libexample_users.dylib:
(__TEXT,__text) section
__Z23forward_to_use_as_valueRKNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE:
0000000000000e70 pushq %rbp
0000000000000e71 movq %rsp, %rbp
0000000000000e74 movzbl (%rdi), %esi
0000000000000e77 testb $0x1, %sil
0000000000000e7b je 0xe8b
0000000000000e7d movq 0x8(%rdi), %rsi
0000000000000e81 movq 0x10(%rdi), %rdi
0000000000000e85 popq %rbp
0000000000000e86 jmp 0xf60 ## symbol stub for: __Z12use_as_value11string_view
0000000000000e8b incq %rdi
0000000000000e8e shrq %rsi
0000000000000e91 popq %rbp
0000000000000e92 jmp 0xf60 ## symbol stub for: __Z12use_as_value11string_view
0000000000000e97 nopw (%rax,%rax)
__Z27forward_to_use_as_const_refRKNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE:
0000000000000ea0 pushq %rbp
0000000000000ea1 movq %rsp, %rbp
0000000000000ea4 subq $0x10, %rsp
0000000000000ea8 movzbl (%rdi), %eax
0000000000000eab testb $0x1, %al
0000000000000ead je 0xebd
0000000000000eaf movq 0x10(%rdi), %rax
0000000000000eb3 movq %rax, -0x10(%rbp)
0000000000000eb7 movq 0x8(%rdi), %rax
0000000000000ebb jmp 0xec7
0000000000000ebd incq %rdi
0000000000000ec0 movq %rdi, -0x10(%rbp)
0000000000000ec4 shrq %rax
0000000000000ec7 movq %rax, -0x8(%rbp)
0000000000000ecb leaq -0x10(%rbp), %rdi
0000000000000ecf callq 0xf66 ## symbol stub for: __Z16use_as_const_refRK11string_view
0000000000000ed4 addq $0x10, %rsp
0000000000000ed8 popq %rbp
0000000000000ed9 retq
0000000000000eda nopw (%rax,%rax)
而且,同样,按值版本短了几条指令。
在我看来,至少通过指令计数的粗略度量,按值版本为调用者和生成的函数体生成更好的代码。
我当然愿意接受有关如何改进此测试的建议。显然,下一步是将其重构为我可以对其进行有意义的基准测试的东西。我会尽快尝试这样做。
我将使用某种构建脚本将示例代码发布到 github,以便其他人可以在他们的系统上进行测试。
但基于上面的讨论,以及检查生成的代码的结果,我的结论是,按值传递是视图类型的方法。
撇开关于常量与价值作为函数参数的信号值的哲学问题,我们可以看看ABI对各种架构的一些影响。
http://www.macieira.org/blog/2012/02/the-value-of-passing-by-value/列出了一些QT人员在x86-64,ARMv7硬浮子,MIPS硬浮点(o32(和IA-64上所做的一些决策和测试。 大多数情况下,它检查函数是否可以通过寄存器传递各种结构。 毫不奇怪,似乎每个平台都可以通过寄存器管理 2 个指针。 鉴于 sizeof(size_t( 通常是 sizeof(void*(,我们几乎没有理由相信我们会在这里溢出到内存中。
我们可以为火找到更多的木材,考虑以下建议:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3538.html。 请注意,const ref 有一些缺点,即混叠的风险,这可能会阻止重要的优化,并且需要程序员额外考虑。 在没有对 C99 限制的C++支持的情况下,按值传递可以提高性能并降低认知负荷。
我想我正在综合两个支持按值传递的论点:
- 32位平台通常缺乏通过寄存器传递两个字结构的能力。 这似乎不再是一个问题。
- 常量引用在数量和质量上都比值差,因为它们可以混叠。
所有这些都会导致我倾向于按值传递整型的 <16 字节结构。 显然,您的里程可能会有所不同,并且应始终在性能有问题的情况下进行测试,但对于非常小的类型,值似乎更好一些。
除了这里已经说过的赞成按值传递的内容之外,现代C++优化器还在为引用参数而苦苦挣扎。
当被调用方的正文在翻译单元中不可用时(函数驻留在共享库或其他翻译单元中,并且链接时间优化不可用(,将发生以下情况:
- 优化程序假定通过引用或引用 const 传递的参数可以更改(
const
无关紧要,因为const_cast
(或由全局指针引用,或由另一个线程更改。基本上,通过引用传递的参数在调用站点中成为中毒值,优化器无法再对其应用许多优化。 - 在被调用方中,如果有多个相同基类型的引用/指针参数,优化程序假定它们与其他参数别名,这再次排除了许多优化。
- 此外,所有
char
类型的数组都可以别名任何其他类型的值,因此修改任何std::string
对象意味着修改任何和所有其他对象,导致以下机器代码必须从内存中重新加载所有对象。restrict
关键字被添加到C
中,以解决这种低效率问题。不同的地址可能仍然是别名,因为可以将一个页面帧多次映射到一个虚拟地址空间(这是 0-copy 接收环形缓冲区的流行技巧(,这就是为什么编译器不能假设不同地址没有别名的原因,除非使用restrict
关键字。
从优化器的角度来看,按值传递和返回是最好的,因为这消除了别名分析的需要:调用方和被调用方独占地拥有其值的副本,因此无法从其他任何地方修改这些值。
对于这个主题的详细处理,我不能推荐钱德勒·卡鲁斯:优化C++的涌现结构。演讲的要点是"人们需要改变对价值传递的看法......传递参数的寄存器模型已过时.">
以下是我将变量传递给函数的经验法则:
- 如果变量可以放入处理器的寄存器中并且不会被修改,按值传递。
- 如果要修改变量,则按引用传递。
- 如果变量大于处理器的寄存器,并且不会被修改,通过常量引用传递。
- 如果需要使用指针,请通过智能指针传递。
希望有帮助。
值是一个值,常量引用是一个常量引用。
如果对象不是不可变的,那么这两者就不是等价的概念。
是的。。。即使是通过const
引用接收的对象也可能变异(甚至可以在您手中仍然拥有 const 引用时被销毁(。 const
引用只说明了使用该引用可以做什么,但它并没有说明引用的对象不会变异或不会通过其他方式停止存在。
要查看一个非常简单的情况,其中别名可能会与看似合法的代码严重咬合,请参阅此答案。
您应该在逻辑需要引用的地方使用引用(即对象标识很重要(。当逻辑只需要值时,您应该传递一个值(即对象标识无关紧要(。对于不可变的,通常身份是无关紧要的。
使用引用时,应特别注意混叠和生命周期问题。另一方面,在传递值时,您应该考虑可能涉及复制,因此如果类很大并且这可以证明是程序的严重瓶颈,那么您可以考虑传递 const 引用(并仔细检查别名和生存期问题(。
在我看来,在这种特定情况下(只是几个原生类型(,需要常量引用传递效率的借口很难证明是合理的。很可能所有内容都会被内联,引用只会使事情更难优化。
当被调用方对身份不感兴趣(即未来*状态更改(时指定const T&
参数是设计错误。故意制造此错误的唯一理由是当对象很重并且制作副本是一个严重的性能问题时。
对于小对象,从性能的角度来看,制作副本通常更好,因为间接寻址少了一个,优化器偏执端不需要考虑混叠问题。例如,如果您有 F(const X& a, Y& b)
并且X
包含类型 Y
的成员,则优化程序将被迫考虑非常量引用实际上绑定到 X
的子对象的可能性。
(*( 对于"future",我在从方法返回(即被调用方存储对象的地址并记住它(和在被调用方代码执行期间(即别名(都包括在内。
由于在这种情况下使用哪一个没有丝毫区别,这似乎只是一场关于自我的辩论。这不是应该阻碍代码审查的事情。除非有人测量性能并发现此代码对时间至关重要,否则我非常怀疑。
我的论点是同时使用两者。 更喜欢康斯坦&。 它也将成为文档。 如果你已经将其声明为 const&,那么如果你试图修改实例(当你不打算修改时(,编译器会抱怨。 如果您确实打算修改它,请按值获取它。 但是,通过这种方式,您可以明确地向未来的开发人员传达您打算修改实例的信息。 而且 const& "可能不比价值差",而且可能要好得多(如果构建一个实例很昂贵,而你还没有(。
- 何时应通过引用传递矢量参数而不是按值传递矢量参数?
- C++类 - 初始化列表 - 递归 - 按值传递
- 将函数参数完美转发到函数指针:按值传递呢?
- 棘手的按值传递和按引用递归问题传递
- 不同于按值传递和常量引用传递的程序集
- 按值传递变量与按引用传递变量具有相同的结果
- 为什么按值传递QStringView比引用常量更快?
- 获取 std::函数以推断按引用传递/按值传递
- 在函数中按值传递 unordered_map/unordered_set 是否有效? C++
- C++/11 auto 关键字是在更有效时推导参数进行按引用传递,还是始终按值传递?
- 使用 enable_if 在按值传递与按引用传递之间更改函数声明
- 防止在按值传递对象(继承)时进行切片
- 为什么功能程序的规律性允许按值传递和按常量引用传递?
- 按值或常量引用传递函数
- 为什么我必须声明这些引用参数常量或按值传递
- C++视图类型:按常量传递还是按值传递?
- C++函数重载分辨率,涉及按值传递、引用和常量引用
- 按值或常量引用传递
- 为什么向量被视为按值传递,即使它是通过常量引用传递的
- 按值传递 vs 常量 &和 &&&重载