C++视图类型:通过const&或值传递?

acm*_*acm 51 c++ parameter-passing pass-by-reference pass-by-value c++11

这最近出现在代码审查讨论中,但没有令人满意的结论.有问题的类型是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;
};
Run Code Online (Sandbox Code Playgroud)

出现的问题是,是否有一种方法可以通过值或const引用来传递这些视图类型(包括即将发生的string_view和array_view类型).

支持传递值的参数等于"减少输入","如果视图具有有意义的突变,则可以改变本地副本",并且"可能效率不低".

支持pass-by-const-reference的参数相当于"通过const&'传递对象更加惯用,而'可能效率不低'.

是否有任何额外的考虑因素可以通过一种或另一种方式最终通过值或const引用传递惯用视图类型.

对于这个问题,可以安全地假设C++ 11或C++ 14语义,以及足够现代的工具链和目标体系结构等.

Yak*_*ont 34

如有疑问,请按值传递.

现在,你应该很少有疑问.

通常价值昂贵,通过并且收益甚微.有时你实际上想要一个存储在别处的可能变异值的引用.通常,在通用代码中,您不知道复制是否是一项昂贵的操作,因此您不应该这样做.

当有疑问时你应该通过值传递的原因是因为值更容易推理.const当你调用一个函数回调或者你有什么东西时,外部数据的引用(甚至是一个)可能会在算法中间发生变异,将一个简单的函数渲染成复杂的混乱.

在这种情况下,您已经有一个隐式引用绑定(对于您正在查看的容器的内容).添加另一个隐式引用绑定(对于查看容器的视图对象)也同样糟糕,因为已经存在并发症.

最后,编译器可以比关于值的引用更好地推理值.如果你离开本地分析的范围(通过函数指针回调),编译器必须假定存储在const引用中的值可能已完全改变(如果它不能证明相反).可以假设自动存储中没有指向它的指针的值不会以类似的方式修改 - 没有定义的方法来访问它并从外部范围更改它,因此可以假定这种修改不会发生.

当您有机会将值作为值传递时,请接受简单性.它很少发生.

  • 我喜欢关于编译器更好地推理价值观的论点.关于编译器需要假设任何回调可能已经改变了引用对象的观点对我来说似乎相当有说服力. (9认同)

acm*_*acm 18

编辑:代码可在此处获取: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;
};
Run Code Online (Sandbox Code Playgroud)

现在,让我们定义一些消耗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);
Run Code Online (Sandbox Code Playgroud)

这些函数的主体定义如下,在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()));
}
Run Code Online (Sandbox Code Playgroud)

do_something_else这里的函数是对编译器没有洞察力的函数的任意调用的替身(例如来自其他动态对象的函数等).声明在do_something_else.hpp:

#pragma once

void __attribute__((visibility("default"))) do_something_else();
Run Code Online (Sandbox Code Playgroud)

而琐碎的定义是do_something_else.cpp:

#include "do_something_else.hpp"

#include <cstdio>

void do_something_else() {
    std::printf("Doing something\n");
}
Run Code Online (Sandbox Code Playgroud)

我们现在将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)
Run Code Online (Sandbox Code Playgroud)

有趣的是,按值的版本是几个指令更短.但那只是功能机构.打电话怎么样?

我们将定义一些调用这两个重载的函数,转发a const std::string&,in 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);
Run Code Online (Sandbox Code Playgroud)

并将它们定义为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);
}
Run Code Online (Sandbox Code Playgroud)

我们再次编译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
Run Code Online (Sandbox Code Playgroud)

而且,我们再次查看生成的代码:

> 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)
Run Code Online (Sandbox Code Playgroud)

而且,按值的版本再次缩短了几条指令.

在我看来,至少通过指令计数的粗略度量,按值版本为调用者和生成的函数体生成更好的代码.

我当然愿意接受如何改进这项测试的建议.显然,下一步是将其重构为可以对其进行有意义的基准测试.我会尽快尝试这样做.

我将使用某种构建脚本将示例代码发布到github,以便其他人可以在他们的系统上进行测试.

但基于上面的讨论以及检查生成的代码的结果,我的结论是,按值传递是查看类型的方法.


han*_*tmk 13

抛开关于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限制的情况下,通过值传递可以提高性能并降低认知负荷.

我想那时我正在合成两个参数,而不是传递值:

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

所有这些都会让我倾向于支持<16字节整数类型结构的值传递.显然,您的里程可能会有所不同,并且应该始终在性能问题上进行测试,但对于非常小的类型,值看起来确实好一些.


Max*_*kin 10

除了这里已经说过的有利于传递值的东西之外,现代C++优化器还在努力使用引用参数.

当被调用者的主体在翻译单元中不可用时(该功能驻留在共享库或另一个翻译单元中并且链接时优化不可用),会发生以下情况:

  1. 优化器假定通过引用或引用传递给const的参数可以更改(const无关紧要const_cast)或由全局指针引用,或由另一个线程更改.基本上,引用传递的参数在调用站点中变为"中毒"值,优化器不能再应用许多优化.
  2. 在被调用者中,如果存在多个相同基类型的引用/指针参数,则优化器会假定它们使用其他内容进行别名,这再次排除了许多优化.

从优化器的角度来看,传递和返回值是最好的,因为这样就不需要别名分析:调用者和被调用者专门拥有它们的值副本,这样就不能从其他任何地方修改这些值.

对于该主题的详细处理,我不能推荐足够的Chandler Carruth:优化C++的紧急结构.谈话的妙语是"人们需要改变他们关于通过价值传递的头脑......传递参数的寄存器模型已经过时了."


Tho*_*ews 6

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

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

希望有所帮助.

  • #4背后的逻辑是什么?据我所知,你几乎从不想传递智能指针作为参数. (2认同)