C++ 视图类型:按 const& 传递还是按值传递?

C++ view types: pass by const& or by value?

提问人:acm 提问时间:12/3/2014 最后编辑:acm 更新时间:2/15/2022 访问量:15974

问:

这在最近一次代码审查讨论中出现,但没有一个令人满意的结论。所讨论的类型是 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;
};

问题在于,无论哪种方式,是否都倾向于通过值或常量引用来传递此类视图类型(包括即将到来的 string_view 和 array_view 类型)。

支持按值传递的论点相当于“减少键入”、“如果视图有有意义的突变,可以改变本地副本”,以及“可能效率不低”。

支持通过常量引用的论点相当于“通过常量传递对象更习惯”,并且“可能效率不低”。

是否有任何其他考虑因素可能会以一种或另一种方式最终摆动论点,即按值或常量引用传递惯用视图类型是否更好。

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

C++ C++11 参数传递 引用传递值

评论

0赞 Maxim Egorushkin 12/3/2014
在目标环境中调用非内联函数时,为什么不按值和引用传递它?
12赞 screwnut 12/3/2014
在计算机科学史上,“少打字”的论点弊大于利。主要是因为这不是一个论点。人们只需要停止这样说。甚至在开玩笑。
3赞 Mooing Duck 12/3/2014
对我来说,字符串视图是一个参考。除非需要重新拔插引用,否则对引用的引用意义不大。

答:

9赞 Thomas Matthews 12/3/2014 #1

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

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

希望能有所帮助。

评论

0赞 12/3/2014
您可以更正 #2(函数内部的修改,修改(输出)参数)
3赞 James Kanze 12/3/2014
#4 背后的逻辑是什么。据我所知,你几乎从来不想传递一个智能指针作为参数。
0赞 Félix Cantournet 12/3/2014
@JamesKanze 如果所有权是共享/可变的,并且内存是堆分配的,则使用 4。
0赞 Paul J. Lucas 12/3/2014
我个人不喜欢 #2,因为您无法通过查看代码来判断参数是否被修改。如果一个参数被修改,我总是通过指针传递。(因此,我不喜欢参数的非常量引用。
1赞 MaHuJa 12/3/2014
如果不涉及所有权语义,则哑指针非常好。如果函数将保留指针,以便可以在返回后的某个时间访问它,则应需要传递智能指针。否则,传递一个哑指针是好的。
3赞 6502 12/3/2014 #2

值是值,常量引用是常量引用。

如果对象不是不可变的,那么这两者就不是等价的概念。

是的。。。即使是通过引用接收的对象也可以变异(甚至可以在你手中仍然有一个常量引用时被破坏)。 引用仅说明使用该引用可以做什么,它没有说明引用对象不会变异或不会通过其他方式停止存在。constconst

要查看一个非常简单的情况,其中混叠可能会严重影响看似合法的代码,请参阅此答案

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

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

在我看来,在这种特定情况下(只有几种原生类型),需要常量引用传递效率的借口很难证明是合理的。最有可能的是,无论如何,一切都会被内联,而引用只会使事情更难优化。

当被调用方对标识不感兴趣时指定参数(即未来*状态更改)是设计错误。故意犯此错误的唯一理由是,当对象很重并且进行复制是一个严重的性能问题时。const T&

对于小对象,从性能的角度来看,制作副本实际上通常更好,因为少了一个间接,而且优化器偏执的一面不需要考虑混叠问题。例如,如果具有并包含 类型的成员,则优化器将被迫考虑非 const 引用实际上绑定到 的子对象的可能性。F(const X& a, Y& b)XYX

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

评论

0赞 acm 12/3/2014
我理解其中的区别,并没有要求澄清函数参数的语义。
0赞 6502 12/3/2014
如果你知道它们是不同的概念,那么问你是否应该使用一个或另一个是没有意义的。两者都需要,具体取决于上下文。不幸的是,C++推动了这样一种观点,即a是一种很酷的说法,这种错误甚至在标准库本身中也存在。const T&T
1赞 acm 12/3/2014
F(T t) 和 F(const T&t) 具有非常相似的语义,但当然并不完全相同。作为一个编写 F 的人,其中任何一个声明都适合预期的实现,我必须在这些声明之间做出选择。我问的是,当两者都合适时,对于一类狭窄的类型(指针 + 大小),是否有理由更喜欢一个选择而不是另一个选择。
1赞 6502 12/3/2014
@acm:语义确实大不相同;在 你正在传达一个对象的地址,并且可以访问当前和未来的状态,而你只传达当前状态。如果你检查链接答案中的示例,你应该注意到使用在概念上是错误的(而且它反咬一口)。对值使用 const 引用的唯一借口是效率,但在您的情况下不成立,有了值,编译器会做得更好。F(const T&)FF(T)rect-=(const P2d&)
1赞 6502 12/3/2014
@acm:就像我已经写的那样,当被调用者对身份不感兴趣时(即未来的状态变化)指定一个是一个设计错误。唯一的理由是当对象很重并且制作副本是一个严重的性能问题时。对于小对象,从性能的角度来看,制作副本实际上通常更好,因为少了一个间接,并且优化器偏执的一方不需要考虑混叠问题(例如,如果您有,优化器将考虑两个引用指向同一对象的可能性)。const T&F(const X& a, X& b)
0赞 gnasher729 12/3/2014 #3

由于在这种情况下使用哪一个没有丝毫区别,这似乎只是一场关于自我的辩论。这不应该阻碍代码审查。除非有人测量性能并发现这段代码对时间至关重要,否则我非常非常怀疑。

评论

0赞 acm 12/3/2014
我没有说它阻碍了代码审查。这是一次附带讨论。我的问题不是关于正确性,而是关于成语。
2赞 newacct 12/3/2014
“既然在这种情况下使用哪一个没有丝毫区别”,你为什么这么说?
53赞 Yakk - Adam Nevraumont 12/3/2014 #4

如有疑问,请按值传递。

现在,你应该很少怀疑。

通常,通过价值的成本很高,而且几乎没有好处。有时,您实际上希望引用存储在其他地方的可能发生变异的值。通常,在通用代码中,您不知道复制是否是一项代价高昂的操作,因此您犯了错误。

有疑问时应该按值传递的原因是,值更容易推理。当您调用函数回调或您拥有的东西时,对外部数据的引用(即使是一个)可能会在算法中间发生变化,从而将看似简单的函数变成复杂的混乱。const

在本例中,您已经有一个隐式引用绑定(到您正在查看的容器的内容)。添加另一个隐式引用绑定(到查看容器的视图对象)同样糟糕,因为已经存在复杂性。

最后,编译器可以更好地推理值,而不是对值的引用。如果离开本地分析的范围(通过函数指针回调),编译器必须假定存储在常量引用中的值可能已完全更改(如果它无法证明相反)。在自动存储中,没有人接受指向它的指针的值,可以假定它不会以类似的方式进行修改 -- 没有定义的方式来访问它并从外部作用域更改它,因此可以假定不会发生此类修改。

当您有机会将值作为值传递时,请拥抱简单性。这种情况很少发生。

评论

10赞 acm 12/3/2014
我更喜欢关于编译器对值进行推理的论点。关于编译器需要假设任何回调都可能使引用的对象发生变异的观点对我来说似乎很有说服力。
1赞 Thomas Matthews 12/3/2014
不应按值传递大型对象。创建指针和引用是为了防止不必要的大型结构/对象复制。
0赞 acm 12/3/2014
@ThomasMatthews 这个问题明确是关于小物体的。
0赞 acm 12/3/2014
@LightnessRacesinOrbit 是的,对我来说也是如此。基于上面的评论,以及我在下面的 codegen 实验结果,最初的代码审查讨论似乎已经强烈支持始终按值传递的视图类型。
0赞 seand 12/3/2014
我同意按值传递小对象是最好的。什么是小的?也许低于 128 位?对小对象使用 &const 会导致间接影响,这可能会减慢速度。
0赞 Andre Kostur 12/3/2014 #5

我的论点是两者兼而有之。首选 const&。它也可以是文档。如果您已将其声明为 const&,则如果您尝试修改实例(当您不打算修改时),编译器将抱怨。如果您确实打算修改它,请按值获取它。但这样一来,您就明确地向未来的开发人员传达了您打算修改实例的信息。const& “可能不比价值差”,而且可能更好(如果构建一个实例很昂贵,而你还没有一个实例)。

评论

1赞 acm 12/3/2014
是的,但您始终可以声明它采用常量值。
1赞 Andre Kostur 12/3/2014
@acm 没错。然后回到对建设成本的考虑。如果构建成本很高,那么 const& 可能是一个更好的主意。(撇开多线程问题不谈。这增加了一个完全不同的考虑因素。
0赞 acm 12/3/2014
右。总的来说,我同意你的看法。特别是对于模板中的任意类型 T,您无法知道按值传递 T 的成本,T const& 很可能总是更可取的。但是,我对我们都知道确切类型的特殊情况感兴趣,并且我们知道该类型的表示恰好是一个指针和一个size_t。
0赞 underscore_d 6/16/2019
"按值计算。但这样一来,你就明确地向未来的开发人员传达了你打算修改实例的信息。...什么?不,你不是。你只是在告诉他们,你会拿走他们传递的内容的副本。你用它做什么不关他们的事,因为它是他们的副本。如果不想修改它,请在实现中声明参数。这应该是习惯,除非你真的需要修改副本。但是,无论哪种方式,您都有一个不同的实例,呼叫者不关心该实例。const
16赞 hanumantmk 12/3/2014 #6

撇开关于常量与值作为函数参数的信号值的哲学问题不谈,我们可以看看 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. 常量引用在数量和质量上都比值差,因为它们可以别名。

所有这些都会导致我赞成整数类型的 <16 字节结构的按值传递。显然,您的里程可能会有所不同,并且应始终在性能有问题的情况下进行测试,但对于非常小的类型,数值似乎确实更好一些。

评论

1赞 underscore_d 6/16/2019
鉴于您的观点 #1,您会说在 64 位平台上,于或等于 16 字节的结构可能应该按值而不是引用传递吗?这往往是我的经验法则:2个词,无论它们在相关平台上有多长。这是基于一个假设,即 2 个单词要么可以放入扩展寄存器,要么足够便宜以复制间接性,无论如何都更糟。
32赞 acm 12/3/2014 #7

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

我创建了一些示例代码,这些代码似乎表明,对类似对象string_view进行按值传递可以为至少一个平台上的调用者和函数定义提供更好的代码。

首先,我们在以下位置定义一个假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_elsedo_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和示例.cpp编译到单独的动态库中。编译器是 OS X Yosemite 6 上的 XCode 10.10.1:

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

但基于上面的讨论,以及检查生成的代码的结果,我的结论是,按值传递是视图类型的必经之路。

评论

0赞 Radek Strugalski 9/14/2022
我也观察到,按值传递版本生成的代码比按引用传递的代码短。然而,令我惊讶的是,较长的代码比具有传递值的代码运行速度略快。因此,正如有人已经指出的那样(不确定是否在此线程中),指令计数并不总是意味着它会运行得更快。
12赞 Maxim Egorushkin 12/3/2014 #8

除了这里已经说过的赞成按值传递的内容之外,现代 C++ 优化器还在与引用参数作斗争。

当被调用方的正文在翻译单元中不可用时(该函数驻留在共享库或其他翻译单元中,并且链接时间优化不可用),则会发生以下情况:

  1. 优化器假定通过引用或引用传递给 const 的参数可以更改( 无关紧要,因为 ) 或由全局指针引用,或由另一个线程更改。基本上,通过引用传递的参数会成为调用站点中的中毒值,优化程序无法再对其应用许多优化。constconst_cast
  2. 在被调用方中,如果存在多个相同基类型的引用/指针参数,则优化器会假定它们与其他参数别名,这再次排除了许多优化。
  3. 此外,所有类型数组都可以为任何其他类型的值添加别名,因此修改任何对象都意味着修改任何其他对象,导致以下机器代码必须从内存中重新加载所有对象。 添加了关键字,以解决这种低效率问题。不同的地址可能仍然是别名,因为可以将一个页面帧多次映射到一个虚拟地址空间中(这是 0 副本接收环缓冲区的流行技巧),这就是为什么编译器不能假设不同地址没有别名,除非使用关键字。charstd::stringrestrictCrestrict

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

对于这个主题的详细处理,我不能推荐足够的钱德勒·卡鲁斯:优化C++的涌现结构。演讲的重点在于“人们需要改变对价值传递的看法......传递参数的寄存器模型已经过时了。