繁体   English   中英

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

[英]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限制的情况下,通过值传递可以提高性能并降低认知负荷。

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

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

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

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

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

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

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

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

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

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

希望有所帮助。

值是值,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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM