C++视图类型:按常量传递还是按值传递?

C++ view types: pass by const& or by value?

本文关键字:按值传递 常量 视图 类型 C++      更新时间:2023-10-16

这是在最近的代码审查讨论中提出的,但没有一个令人满意的结论。所讨论的类型类似于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++支持的情况下,按值传递可以提高性能并降低认知负荷。

我想我正在综合两个支持按值传递的论点:

  1. 32位平台通常缺乏通过寄存器传递两个字结构的能力。 这似乎不再是一个问题。
  2. 常量引用在数量和质量上都比值差,因为它们可以混叠。

所有这些都会导致我倾向于按值传递整型的 <16 字节结构。 显然,您的里程可能会有所不同,并且应始终在性能有问题的情况下进行测试,但对于非常小的类型,值似乎更好一些。

除了这里已经说过的赞成按值传递的内容之外,现代C++优化器还在为引用参数而苦苦挣扎。

当被调用方的正文在翻译单元中不可用时(函数驻留在共享库或其他翻译单元中,并且链接时间优化不可用(,将发生以下情况:

  1. 优化程序假定通过引用或引用 const 传递的参数可以更改(const无关紧要,因为const_cast(或由全局指针引用,或由另一个线程更改。基本上,通过引用传递的参数在调用站点中成为中毒值,优化器无法再对其应用许多优化。
  2. 在被调用方中,如果有多个相同基类型的引用/指针参数,优化程序假定它们与其他参数别名,这再次排除了许多优化。
  3. 此外,所有char类型的数组都可以别名任何其他类型的值,因此修改任何std::string对象意味着修改任何和所有其他对象,导致以下机器代码必须从内存中重新加载所有对象。 restrict关键字被添加到C中,以解决这种低效率问题。不同的地址可能仍然是别名,因为可以将一个页面帧多次映射到一个虚拟地址空间(这是 0-copy 接收环形缓冲区的流行技巧(,这就是为什么编译器不能假设不同地址没有别名的原因,除非使用restrict关键字。

从优化器的角度来看,按值传递和返回是最好的,因为这消除了别名分析的需要:调用方和被调用方独占地拥有其值的副本,因此无法从其他任何地方修改这些值。

对于这个主题的详细处理,我不能推荐钱德勒·卡鲁斯:优化C++的涌现结构。演讲的要点是"人们需要改变对价值传递的看法......传递参数的寄存器模型已过时.">

以下是我将变量传递给函数的经验法则:

  1. 如果变量可以放入处理器的寄存器中并且不会被修改,按值传递。
  2. 如果要修改变量,则按引用传递。
  3. 如果变量大于处理器的寄存器,并且不会被修改,通过常量引用传递。
  4. 如果需要使用指针,请通过智能指针传递。

希望有帮助。

值是一个值,常量引用是一个常量引用。

如果对象不是不可变的,那么这两者就不是等价的概念。

是的。。。即使是通过const引用接收的对象也可能变异(甚至可以在您手中仍然拥有 const 引用时被销毁(。 const引用只说明了使用该引用可以做什么,但它并没有说明引用的对象不会变异或不会通过其他方式停止存在。

要查看一个非常简单的情况,其中别名可能会与看似合法的代码严重咬合,请参阅此答案。

您应该在逻辑需要引用的地方使用引用(即对象标识很重要(。当逻辑只需要值时,您应该传递一个值(即对象标识无关紧要(。对于不可变的,通常身份是无关紧要的。

使用引用时,应特别注意混叠和生命周期问题。另一方面,在传递值时,您应该考虑可能涉及复制,因此如果类很大并且这可以证明是程序的严重瓶颈,那么您可以考虑传递 const 引用(并仔细检查别名和生存期问题(。

在我看来,在这种特定情况下(只是几个原生类型(,需要常量引用传递效率的借口很难证明是合理的。很可能所有内容都会被内联,引用只会使事情更难优化。

当被调用方对身份不感兴趣(即未来*状态更改(时指定const T&参数是设计错误。故意制造此错误的唯一理由是当对象很重并且制作副本是一个严重的性能问题时。

对于小对象,从性能的角度来看,制作副本通常更好,因为间接寻址少了一个,优化器偏执端不需要考虑混叠问题。例如,如果您有 F(const X& a, Y& b) 并且X包含类型 Y 的成员,则优化程序将被迫考虑非常量引用实际上绑定到 X 的子对象的可能性。

(*( 对于"future",我在从方法返回(即被调用方存储对象的地址并记住它(和在被调用方代码执行期间(即别名(都包括在内。

由于在这种情况下使用哪一个没有丝毫区别,这似乎只是一场关于自我的辩论。这不是应该阻碍代码审查的事情。除非有人测量性能并发现此代码对时间至关重要,否则我非常怀疑。

我的论点是同时使用两者。 更喜欢康斯坦&。 它也将成为文档。 如果你已经将其声明为 const&,那么如果你试图修改实例(当你不打算修改时(,编译器会抱怨。 如果您确实打算修改它,请按值获取它。 但是,通过这种方式,您可以明确地向未来的开发人员传达您打算修改实例的信息。 而且 const& "可能不比价值差",而且可能要好得多(如果构建一个实例很昂贵,而你还没有(。