[英]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;
};
出现的问题是,是否有一种方法可以通过值或const引用来传递这些视图类型(包括即将发生的string_view和array_view类型)。
支持传递值的参数等于“减少输入”,“如果视图具有有意义的突变,则可以改变本地副本”,并且“可能效率不低”。
支持pass-by-const-reference的参数相当于“通过const&'传递对象更加惯用,而'可能效率不高'。
是否有任何额外的考虑因素可以通过一种或另一种方式最终通过值或const引用传递惯用视图类型。
对于这个问题,可以安全地假设C ++ 11或C ++ 14语义,以及足够现代的工具链和目标体系结构等。
如有疑问,请按值传递。
现在,你应该很少有疑问。
通常价值昂贵,通过并且收益甚微。 有时你实际上想要一个存储在别处的可能变异值的引用。 通常,在通用代码中,您不知道复制是否是一项昂贵的操作,因此您不应该这样做。
当有疑问时你应该通过值传递的原因是因为值更容易推理。 当你调用一个函数回调或者你有什么东西时,外部数据的引用(甚至是const
)可能会在算法中间发生变异,从而将一个简单的函数渲染成复杂的混乱。
在这种情况下,您已经有一个隐式引用绑定(对于您正在查看的容器的内容)。 添加另一个隐式引用绑定(对于查看容器的视图对象)也同样糟糕,因为已经存在并发症。
最后,编译器可以比关于值的引用更好地推理值。 如果你离开本地分析的范围(通过函数指针回调),编译器必须假定存储在const引用中的值可能已完全改变(如果它不能证明相反)。 可以假设自动存储中没有指向它的指针的值不会以类似的方式修改 - 没有定义的方法来访问它并从外部范围更改它,因此可以假定这种修改不会发生。
当您有机会将值作为值传递时,请接受简单性。 它很少发生。
编辑:代码可在此处获取: https : //github.com/acmorrow/stringview_param
我已经创建了一些示例代码,它们似乎证明了string_view类似对象的pass-by-value可以在至少一个平台上为调用者和函数定义创建更好的代码。
首先,我们定义了一个假string_view类(我没有真实的东西派上用场) string_view.h
:
#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 %zu\n", strchr(view.data(), 'a') - view.data(), view.len(), strlen(view.data()));
do_something_else();
printf("%ld %ld %zu\n", strchr(view.data(), 'a') - view.data(), view.len(), strlen(view.data()));
}
void use_as_const_ref(const string_view& view) {
printf("%ld %ld %zu\n", strchr(view.data(), 'a') - view.data(), view.len(), strlen(view.data()));
do_something_else();
printf("%ld %ld %zu\n", 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 something\n");
}
我们现在将do_something_else.cpp和example.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 %zu\n"
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 %zu\n"
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
转发const std::string&
:
#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,以便其他人可以在他们的系统上进行测试。
但基于上面的讨论以及检查生成的代码的结果,我的结论是,按值传递是查看类型的方法。
抛开关于const&-ness与value-ness的信号值作为函数参数的哲学问题,我们可以看看ABI对各种体系结构的影响。
http://www.macieira.org/blog/2012/02/the-value-of-passing-by-value/列出了一些QT人员在x86-64,ARMv7 hard-float上做出的一些决策和测试, 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有一些缺点,即别名的风险,这可能会阻止重要的优化,并需要程序员额外的思考。 在没有C ++支持C99限制的情况下,通过值传递可以提高性能并降低认知负荷。
我想那时我正在合成两个参数,而不是传递值:
所有这些都会让我倾向于支持<16字节整数类型结构的值传递。 显然,您的里程可能会有所不同,并且应该始终在性能问题上进行测试,但对于非常小的类型,值看起来确实好一些。
除了这里已经说过的有利于传递值的东西之外,现代C ++优化器还在努力使用引用参数。
当被调用者的主体在翻译单元中不可用时(该功能驻留在共享库或另一个翻译单元中并且链接时优化不可用),会发生以下情况:
const
因const_cast
而无关紧要)或由全局指针引用,或由另一个线程更改。 基本上,引用传递的参数在调用站点中变为“中毒”值,优化器不能再应用许多优化。 从优化器的角度来看,传递和返回值是最好的,因为这样就不需要别名分析:调用者和被调用者专门拥有它们的值副本,这样就不能从其他任何地方修改这些值。
对于该主题的详细处理,我不能推荐足够的Chandler Carruth:优化C ++的紧急结构 。 谈话的妙语是“人们需要改变他们关于通过价值传递的头脑......传递参数的寄存器模型已经过时了。”
以下是将变量传递给函数的经验法则:
希望有所帮助。
值是值,const引用是const引用。
如果对象不是不可变的,那么这两个不是等价的概念。
是的...即使是通过const
引用接收的对象也可以变异(或者甚至可以在你手中仍有const引用时被销毁)。 带引用的const
只说明了使用该引用可以做什么,它没有说明引用的对象不会变异或不会通过其他方式停止存在。
要查看一个非常简单的情况,其中别名可能会与显然合法的代码严重咬合,请参阅此答案 。
您应该使用逻辑需要引用的引用(即对象标识很重要)。 当逻辑只需要值时(即对象标识不相关),您应该传递一个值。 对于不可变的,通常身份是无关紧要的。
当您使用引用时,应特别注意别名和生命周期问题。 另一方面,在传递值时,您应该考虑复制可能涉及,因此如果类很大并且这可能是您的程序的严重瓶颈,那么您可以考虑传递const引用(并仔细检查别名和生命周期问题) 。
在我看来,在这个特定的情况下(只是几个原生类型),需要const-reference传递效率的借口很难证明。 最重要的是,无论如何,所有内容都将被内联,而引用只会使事情更难以优化。
当被调用者对身份不感兴趣(即未来*状态更改)时指定const T&
参数是设计错误。 故意制造此错误的唯一理由是当对象很重并且复制是严重的性能问题时。
对于小对象,从性能的角度来看,制作副本通常实际上更好 ,因为有一个间接较少,优化器偏执方不需要考虑别名问题。 例如,如果您有F(const X& a, Y& b)
并且X
包含Y
类型的成员,则优化器将被强制考虑非const引用实际绑定到X
子对象的可能性。
(*)对于“future”,我在从方法返回后(即被调用者存储对象的地址并记住它)和执行被调用者代码(即别名)时包括两者。
因为在这种情况下你使用哪一个没有丝毫差别,这似乎只是一个关于自我的争论。 这不应该阻止代码审查。 除非有人测量性能并且发现这段代码是时间关键的,否则我非常怀疑。
我的论点是同时使用两者。 喜欢const&。 它也可以成为文档。 如果您已将其声明为const&,那么如果您尝试修改实例(当您不打算这样做时),编译器将会抱怨。 如果您打算对其进行修改,请按值进行修改。 但是这样您就可以明确地与未来的开发人员沟通,以便修改实例。 而const&“可能并不比价值更差”,而且可能更好(如果构建一个实例是昂贵的,而你还没有一个)。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.