c++ 参数作为 const T 与 const T&

c++ parameter as const T vs const T&

假设 SomeDataStruct 是“巨大的”,现代编译器是否会在以下两种情况下生成等效且因此同样高效的代码?

1) void fooByValue(const SomeDataStruct data);   

2) void fooByReference(const SomeDataStruct& data);

如果等价,哪个成语是“preferable”,为什么?

请注意这个问题与这个问题类似:

https://softwareengineering.stackexchange.com/questions/372105/is-passing-arguments-as-const-references-premature-optimization

但不完全相同,因为我在这两个函数中都使用了“const”;在提到的link中,fooByValue只是

1) void fooByValue(SomeDataStruct data);

编辑:假设 SomeDataStruct 类型没有任何复制构造函数。

顶层 const(不在 pointer/reference 之后)在声明中无效。

// Even if the declaration uses const:
void fooByValue(const SomeDataStruct data);

// The definition without `const` is still considered a valid definition for that declaration.
// It's not a different overload!
// (this oddity is inherited from C, where overloading is a compiler error but this was always allowed)
void fooByValue(SomeDataStruct data)
{
   data.member = 0;
}

因此编译器被迫忽略 const 并且必须假设参数可能被修改 --> 需要一个副本。 但是,如果在函数调用后未使用原始复制自变量,编译器可能仍会优化副本。

C++ 标准的相关部分是 9.3.3.5 函数:

The type of a function is determined using the following rules. [..] After producing the list of parameter types, any top-level cv-qualifiers modifying a parameter type are deleted when forming the function type.

suppose SomeDataStruct is "huge", will a modern compiler produce equivalent and therefore equally efficient code in the following two cases?

这取决于编译器,你如何定义对象,函数中的对象发生了什么。让我们看看 gcc 在 x86-64 Linux 上做了什么。看看下面的代码:

struct big_
{
    unsigned long w,x,y,z;
    int pos;
};

unsigned long byval(const big_ big)
{
    auto y = big.z;
    y += big.y;
    y += big.x;
    y += big.w;
    return y;
}

unsigned long byref(const big_& big)
{
    auto y = big.z;
    y += big.y;
    y += big.x;
    y += big.w;
    return y;
}

g++ -std=c++17 -O3 编译得到以下程序集 Godbolt link:

byval(big_):
        mov     rax, QWORD PTR [rsp+24]
        add     rax, QWORD PTR [rsp+32]
        add     rax, QWORD PTR [rsp+16]
        add     rax, QWORD PTR [rsp+8]
        ret
byref(big_ const&):
        mov     rax, QWORD PTR [rdi+16]
        add     rax, QWORD PTR [rdi+24]
        add     rax, QWORD PTR [rdi+8]
        add     rax, QWORD PTR [rdi]
        ret

从上面的代码我们能看出什么?

第一个函数 byval 的参数在堆栈上传递。第二个函数的参数通过寄存器 rdi 传递(请参阅 OS 的调用约定以了解原因)。通过寄存器传递任何东西总是比通过堆栈传递更快,因为寄存器更接近 cpu 而堆栈位于缓存或 ram 中的某个位置。因此,通过引用传递在这里更好。如果你有一个小对象(8 字节),按值传递它会更好,因为它无论如何都会通过寄存器传递。只有当您的对象太大以至于无法放入寄存器时才会通过堆栈传递。

我们可以看到的另一件事是 byval 有一个参数不是 const。编译器刚刚删除它。

since i here use "const" in both functions;

从上面的解释中可以看出,这并不重要。

void fooByValue(SomeDataStruct data); edit: suppose SomeDataStruct type does not have any copy constructor.

如果您没有编写复制构造函数,那并不意味着编译器不能为您隐式生成一个。但我们假设您删除了它。所以现在你不能复制它,如果你尝试,你的代码将无法编译。但是,如果您已经定义了一个移动构造函数,您可以移动它。

void fn()
{
    fooByValue( std::move(data) ); // no copy
}

使用const不会改变代码的性能。 const 就像你和编译器之间的契约。当您将某些内容标记为 const 时,编译器将不允许您更改它。如果您以某种方式更改它,它将导致未定义的行为。我建议你去阅读 Arthur O'Dwyer 关于这个主题的这篇文章 const is a contract.