将参数传递给函数时的c ++最佳实践

c++ Best practise when passing in arguments to functions

所以我读了很多关于人们说 const & 总是好的,因为它消除了复制,而按值传递是个坏主意。然后我最近也看了一些帖子说 const & is bad for basic data type int, string, etc.

所以有没有人知道在将参数传递给函数时要遵循什么“最佳实践”或最有效的方法,既包括简单的数据类型,例如 int、double string,也包括更复杂的类型,例如向量、对象和现代 C++ 的指针?

谢谢。

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

规则总结如下:

  1. 对于整型对象和floats/doubles按值传递(下面解释原因)

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

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

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

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

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

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

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

所以像

这样的函数
int func( int a, int b );

将使用 %rdi=a %rsi=b 并且 return 值将在 %rax 中。请注意,如果 func 是 class 的方法,第一个参数将是 this 指针,因此序列将为 %rdi=this %rsi=a %rdx=b 并且 return 值将在 %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    , %rsp
        leaq    24(%rsp), %r12
        movq    %r12, 8(%rsp)
        movq    (%rdi), %r14
        movq    8(%rdi), %rbx
        cmpq    , %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    , %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    [=16=], (%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    , %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

有关最佳做法,请查找 C++ core guidelines

一些一般提示:

  1. 对于大对象,通过 const&(如果需要可变性,则通过 &),这是因为复制对象比复制指向对象的指针更昂贵。 (注意 会绑定到 const&,你可以提供一个 && 重载)
  2. 对于较小的(内置类型),通过引用传递没有多大意义,因为对象大小可能与指针具有相同的顺序。
  3. 优先引用指针,以避免需要进行 nullptr 检查

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