C ++视图类型:传递const&或按价值?

时间:2014-12-02 18:25:03

标签: c++ c++11 parameter-passing pass-by-reference pass-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&amp;'传递对象更加惯用”,“可能效率不高”。

是否有任何额外的考虑因素可能会以一种方式最终推断出这个论点,或者通过值或const引用传递惯用视图类型是否更好。

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

8 个答案:

答案 0 :(得分:29)

如有疑问,请按值传递。

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

价值通常很昂贵,而且收益甚微。有时你实际上想要一个存储在别处的可能变异值的引用。通常,在通用代码中,您不知道复制是否是一项昂贵的操作,因此您不会错误。

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

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

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

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

答案 1 :(得分:18)

编辑:代码可在此处获取:https://github.com/acmorrow/stringview_param

我已经创建了一些示例代码,这些代码似乎表明string_view类似对象的值传递会为至少一个平台上的调用者和函数定义带来更好的代码。 / p>

首先,我们在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 %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

#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,以便其他人可以在他们的系统上进行测试。

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

答案 2 :(得分:12)

将关于const&amp; -ness与value-ness的信令值的哲学问题放在一边作为函数参数,我们可以看一下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有一些缺点,即别名的风险,这可能会阻止重要的优化,并需要程序员额外的思考。在没有C ++支持C99限制的情况下,通过值传递可以提高性能并降低认知负荷。

我想那时我正在合成两个参数,支持按值传递:

  1. 32位平台通常缺乏通过寄存器传递两个字结构的能力。这似乎不再是一个问题。
  2. const引用在数量和质量上都比值更差,因为它们可以别名。
  3. 所有这些都会让我倾向于通过整数类型的&lt; 16字节结构的值传递。显然你的里程可能会有所不同,并且应该始终在性能问题上进行测试,但是对于非常小的类型来说,值似乎确实更好。

答案 3 :(得分:8)

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

如果被调用者的主体在翻译单元中不可用(该功能位于共享库或其他翻译单元中,并且链接时优化不可用),则会发生以下情况:

  1. 优化器假定通过引用或引用传递给const的参数可以更改(const由于const_cast无关紧要)或由全局指针引用,或由另一个线程更改。基本上,通过引用传递的参数变为&#34;中毒&#34;调用站点中的值,优化器不能再应用许多优化。
  2. 在被调用者中,如果存在多个相同基类型的引用/指针参数,则优化器会假定它们使用其他内容进行别名,这再次排除了许多优化。
  3. 从优化器的角度来看,传递和返回值是最好的,因为这样就不需要别名分析:调用者和被调用者专门拥有它们的值副本,这样就不能从其他任何地方修改这些值。

    对于受试者的详细治疗,我不能建议Chandler Carruth: Optimizing the Emergent Structures of C++。谈话的妙语是#34;人们需要改变他们关于通过值传递的头脑......传递参数的寄存器模型已经过时了。&#34;

答案 4 :(得分:6)

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

  1. 如果变量可以适合处理器的寄存器,则不会 被修改,按价值传递。
  2. 如果要修改变量,请按引用传递。
  3. 如果变量大于处理器的寄存器,则不会 被修改,通过不断的参考。
  4. 如果你需要使用指针,请通过智能指针。
  5. 希望有所帮助。

答案 5 :(得分:3)

值是值,const引用是const引用。

如果对象不是不可变的,则两者是 NOT 等效概念。

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

要查看一个非常简单的案例,其中别名可能会与显然合法的代码严重咬合,请参阅this answer

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

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

在我看来,在这个特定的情况下(只是几个原生类型),需要const-reference传递效率的借口很难证明。最重要的是,无论如何,所有内容都将被内联,而引用只会使事情更难以优化。

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

对于小型对象,从性能的角度来看,复制通常实际上是更好,因为有一个间接更少,优化器偏执方不需要考虑别名问题。例如,如果F(const X& a, Y& b)X包含类型为Y的成员,则优化程序将被强制考虑非const引用实际绑定到该子对象的可能性。 X

(*)对于“future”,我在从方法返回后(包括被调用者存储对象的地址并记住它)和执行被调用者代码(即别名)时包括两者。

答案 6 :(得分:1)

由于它与你在这种情况下所使用的一个没有丝毫差别,这似乎只是一个关于自我的辩论。这不应该阻止代码审查。除非有人测量性能并且发现这段代码是时间关键的,否则我非常怀疑。

答案 7 :(得分:0)

我的论点是两者同时使用。首选const&amp ;.它也可以成为文档。如果您已将其声明为const&amp;,那么如果您尝试修改实例(当您没有打算这样做时),编译器会抱怨。如果您打算对其进行修改,请按值进行修改。但是这样您就可以明确地与未来的开发人员沟通,以便修改实例。和const&amp;是#34;可能不会更糟糕&#34;而不是价值,可能更好(如果构建一个实例是昂贵的,你还没有一个)。