单行函数模板 c++ 的内联性保证

Inlineness guarantee of one-liner function templates c++

本文关键字:函数模板 c++ 单行      更新时间:2023-10-16

C++标准库中,有许多单行函数模板。 例如std::move 本质上只是一个强制转换,实现可以是:

template<typename _Tp>
constexpr typename std::remove_reference<_Tp>::type&&
move(_Tp&& __t) noexcept
{ return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }

我知道实际上,不会从std::move生成机器代码,因为它只是一个强制转换。我的问题是:标准中是否有任何保证,对于像 std::movestd::forward这样的函数(只做转换(,它们必须始终是内联的(所以不会生成机器代码(?换句话说,(迂腐的(编译器是否可以将它们视为普通函数(即将参数放在堆栈上,并生成调用ret指令(?

我的问题是:标准中是否有任何保证,对于像std::movestd::forward这样的函数(只做强制转换(,它们必须始终是内联的(所以不会生成机器代码(?

不。该标准止步于描述抽象机器的可观察行为。代码生成是一个实现细节,标准对此一无所知。

话虽如此,std::forwardstd::move都是实际上只影响C++类型系统而不是实际数据的操作,因此,如果在优化的构建中看到为它们生成的任何机器代码,我会非常惊讶。

另一方面,在未优化的版本中,保留概述std::move(与几乎任何其他函数调用一样(可能是简化调试的好主意。你可以很容易地测试这个(在gcc.godbolt上直播(:

#include <utility>
struct Foo {
int i;
Foo(Foo &&other) :i(other.i) {};
};
Foo with_move(Foo f) {
return std::move(f);
}

在 gcc 中,带有 -O0std::move作为实际函数生成(除了设置/拆除堆栈帧并返回它收到的指针参数外,它什么都不做(

Foo::Foo(Foo&&):
push    rbp
mov     rbp, rsp
mov     QWORD PTR [rbp-8], rdi
mov     QWORD PTR [rbp-16], rsi
mov     rax, QWORD PTR [rbp-16]
mov     edx, DWORD PTR [rax]
mov     rax, QWORD PTR [rbp-8]
mov     DWORD PTR [rax], edx
nop
pop     rbp
ret
with_move(Foo):
push    rbp
mov     rbp, rsp
sub     rsp, 16
mov     QWORD PTR [rbp-8], rdi
mov     QWORD PTR [rbp-16], rsi
mov     rax, QWORD PTR [rbp-16]
mov     rdi, rax
call    std::remove_reference<Foo&>::type&& std::move<Foo&>(Foo&)
mov     rdx, rax
mov     rax, QWORD PTR [rbp-8]
mov     rsi, rdx
mov     rdi, rax
call    Foo::Foo(Foo&&)
mov     rax, QWORD PTR [rbp-8]
leave
ret
std::remove_reference<Foo&>::type&& std::move<Foo&>(Foo&):
push    rbp
mov     rbp, rsp
mov     QWORD PTR [rbp-8], rdi
mov     rax, QWORD PTR [rbp-8]
pop     rbp
ret

而即使在 -O1 时,所有内容也会内联:

with_move(Foo):
mov     rax, rdi
mov     edx, DWORD PTR [rsi]
mov     DWORD PTR [rdi], edx
ret

这不是来自标准,而是来自斯科特迈耶的有效CPP。 第 30 项的一些
摘录:了解内联的来龙去脉。

编译器优化是 通常设计用于缺少函数调用的代码段,因此 当您内联函数时,您可以使编译器执行上下文- 对函数主体的特定优化。大多数编译器 切勿对"概述"函数调用执行此类优化。

在内存有限的机器上,过分热心 内联可能会导致程序太大而无法使用 空间。即使使用虚拟内存,内联引起的代码膨胀也可能导致 到额外的分页,降低指令缓存命中率,以及 伴随这些事情的绩效处罚。

另一方面,如果内联函数体非常短,则代码 为函数体生成的代码可能小于生成的代码 用于函数调用。如果是这种情况,内联函数可能会 实际上会导致更小的目标代码和更高的指令缓存命中 率!

请记住,内联是对编译器的请求,而不是命令。 ...
模板实例化与内联无关。如果你正在写一个 模板,您认为所有函数都从 模板应该是内联的,声明模板是内联的;

但是,如果您正在为没有理由想要内联的函数编写模板,请避免以内联方式(显式或隐式(声明模板。内联 有成本,你不想在没有深思熟虑的情况下产生它们。

让我们完成内联是一个请求的观察 编译器可能会忽略。大多数编译器拒绝内联函数 他们认为太复杂了(例如,那些包含循环或递归的(, 除了对虚函数的最微不足道的调用之外,所有调用都无视内联

这一切加起来就是:给定的内联函数是否真的是 内联取决于您使用的构建环境 — 主要基于 编译器。幸运的是,大多数编译器的诊断级别是 如果它们无法内联函数,将导致警告 你已经要求他们这样做了。

有时编译器会为内联函数生成函数体 即使他们完全愿意内联函数。例如 如果您的程序采用内联函数的地址,编译器 通常必须为其生成概述的函数体。

如果你能拿到这本书,阅读整篇文章,它会消除你的许多疑虑。