【问题标题】:c++ Best practise when passing in arguments to functions [closed]将参数传递给函数时的c ++最佳实践[关闭]
【发布时间】:2022-01-25 05:04:41
【问题描述】:

所以我读过很多关于人们说 const & 总是好的东西,因为它消除了复制,而按值传递是一个坏主意。然后我最近也看了一些帖子说 const & 对基本数据类型 int、string 等不好。

那么有谁知道在将参数传递给函数时要遵循的“最佳实践”或最有效的方法,包括简单的数据类型,如 int、双字符串和更复杂的类型,如向量、对象和指针现代 c++?

谢谢。

【问题讨论】:

  • 更多的是启发式而不是答案:简单的数据类型(按值),复杂的数据类型(按引用),const 表示您不会修改的任何内容。

标签: c++ oop reference arguments constants


【解决方案1】:

要回答这个问题,您必须查看底层平台。适用于一个的技术和实践可能不适用于另一个。因此,为了便于讨论,让我们关注 x86_64 和传递整数类型参数。其他架构和调用约定可以类似。

规则总结如下:

  1. 对于整型对象和浮点数/双精度值传递(解释原因如下)

  2. 对于像 std::optional 这样较小的对象和仅包含类型 1 值的小型结构,您仍然可以按值传递。

  3. 对于任何较大的对象,通过 const 引用或引用传递。

  4. 特别是对于 std::string,在你的函数中使用 std::string_view 因为它允许你传递一个 char 指针或 char 数组并且不会创建 std::string 临时。

  5. 现代 C++ 引入了“移动语义”和 && 运算符。这会创建其他类的对象,允许您“接管”传递参数的内容,而不是制作副本。这种技术对于大型物体非常有用。

下面有更详细的解释。仅使用整数(包括指针)调用方法时,使用以下寄存器序列:%rdi、%rsi、%rdx、%rcx、%r8 和 %r9。

对于返回值,使用 %rax 和 %rdx。所有这些寄存器都是 64 位的。

这与 i386 调用语义背道而驰,在 i386 调用语义中,所有内容都在堆栈上传递,即在调用之前必须将每个参数存储在内存中。随着 AMD64 ABI 实现寄存器传递,它变得更快了,因为所有操作都在 CPU 内核内部进行,无需访问内存。

这样的函数

int func( int a, int b );

将使用%rdi=a %rsi=b 并且返回值将在%rax 中。注意,如果func是一个类的方法,第一个参数是this指针,所以序列是%rdi=this%rsi=a%rdx=b,返回值是%rax

如果你通过引用传递那个 int 会发生什么?比较一下吧。

int func( int a, int b ) {
    return a+b;
}

int func( int& a, int& b ) {
    return a+b;
}

编译时会产生

func(int, int):                              # @func(int, int)
        leal    (%rdi,%rsi), %eax
        retq

func(int const&, int const&):                # @func(int const&, int const&)
        movl    (%rsi), %eax
        addl    (%rdi), %eax
        retq

所以请注意,引用是作为 POINTER 传递的,这将导致两个更昂贵的操作,即内存提取 movl (%rsi), %eax 加上 add 本身,而不是一个简单的求和 leal (%rdi,%rsi), %eax,无需访问内存即可完成。

因此,在这种情况下,在处理整数(类 int)值时,通过值而不是引用传递速度和缓存使用率要好得多。

上述同样适用于浮点数和双精度数。寄存器不同(使用 %xmm0 等),但适用相同的逻辑。

对于较大的对象,如 std::vector 或 std::string,如果您不打算在该函数或方法的主体中修改此对象,建议通过 const 引用传递。如果您需要修改它们,请通过引用传递。这样做,整数类型的相同规则将适用,因为指针和引用被认为是类似整数的。

例如

#include <string>
int len( const std::string& s ) {
    return s.size();
}

int call( const std::string& s ) {
    return len(s);
}

将屈服于

len(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&): # @len(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)
        movl    8(%rdi), %eax
        retq
call(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&): # @call(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)
        movl    8(%rdi), %eax
        retq

但是如果你像这样通过值传递一个字符串

int len2( std::string s ) {
    return s.size();
}

int call2( std::string s ) {
    return len2(s);
}

那么len2 方法本身仍然很简单,但调用者必须复制它并导致调用者非常大

len2(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >): # @len2(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)
        movl    8(%rdi), %eax
        retq

call2(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >): # @call2(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)
        pushq   %r15
        pushq   %r14
        pushq   %r12
        pushq   %rbx
        subq    $40, %rsp
        leaq    24(%rsp), %r12
        movq    %r12, 8(%rsp)
        movq    (%rdi), %r14
        movq    8(%rdi), %rbx
        cmpq    $15, %rbx
        jbe     .LBB7_1
        testq   %rbx, %rbx
        js      .LBB7_12
        movq    %rbx, %rdi
        incq    %rdi
        js      .LBB7_13
        callq   operator new(unsigned long)
        movq    %rax, %r15
        movq    %rax, 8(%rsp)
        movq    %rbx, 24(%rsp)
        testq   %rbx, %rbx
        jne     .LBB7_6
        jmp     .LBB7_9
.LBB7_1:                                # %entry.if.end_crit_edge.i.i
        movq    %r12, %r15
        testq   %rbx, %rbx
        je      .LBB7_9
.LBB7_6:                                # %if.end.i.i
        cmpq    $1, %rbx
        jne     .LBB7_8
        movb    (%r14), %al
        movb    %al, (%r15)
        jmp     .LBB7_9
.LBB7_8:                                # %if.end.i.i.i.i.i
        movq    %r15, %rdi
        movq    %r14, %rsi
        movq    %rbx, %rdx
        callq   memcpy@PLT
.LBB7_9:                                # %_ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEC2ERKS4_.exit
        movq    %rbx, 16(%rsp)
        movb    $0, (%r15,%rbx)
        movq    8(%rsp), %rdi
        movq    16(%rsp), %rbx
        cmpq    %r12, %rdi
        je      .LBB7_11
        callq   operator delete(void*)
.LBB7_11:                               # %_ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEED2Ev.exit
        movl    %ebx, %eax
        addq    $40, %rsp
        popq    %rbx
        popq    %r12
        popq    %r14
        popq    %r15
        retq
.LBB7_13:                               # %if.end.i.i.i.i.i.i
        callq   std::__throw_bad_alloc()
.LBB7_12:                               # %if.then.i.i.i
        movl    $.L.str, %edi
        callq   std::__throw_length_error(char const*)
.L.str:
        .asciz  "basic_string::_M_create"

参考:https://uclibc.org/docs/psABI-x86_64.pdf

【讨论】:

    【解决方案2】:

    如需最佳实践,请查看C++ core guidelines

    一些一般提示:

    1. 对于大型对象,通过const&amp;(或&amp;,如果您需要可变性),这是因为复制对象将比复制指向对象的指针更昂贵。 (注意r-value objects 将绑定到const&amp;,您可以提供&amp;&amp; 重载)
    2. 对于较小的(内置类型),通过引用传递通常没有多大意义,因为对象大小可能与指针的顺序相同。
    3. 首选对指针的引用,以避免需要进行nullptr 检查

    如果您正在编写模板代码,请查找 perfect forwarding。如果您使用的是 STL 容器,则有像 std::spanstd::string_view 这样的代理对象,它们提供容器的通用视图并且可以按值传递。

    【讨论】:

    • 对于较小的类型,引用可能会导致额外级别的取消引用,从而降低引用效率。
    • @MarkRansom 公平点,但在许多情况下,我将 const&amp; 用于内置或小型 STL 类型 (std::pair),我看不出汇编输出有任何差异。
    • 所以优化器足够聪明,可以捕捉到这种情况并转换代码?这很好,因为这可能是模板代码的常见情况,尤其是标准容器。
    猜你喜欢
    • 2023-03-08
    • 2021-05-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-01-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多