一线功能模板的内联性保证C ++

时间:2018-10-03 01:39:10

标签: c++

在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 :: move std :: forward 之类的功能(仅执行强制转换),它们必须总是内联的(因此不会生成任何机器代码)?换句话说,(pedantic)编译器是否有可能将它们视为普通函数(即,将参数放在堆栈上,并生成 call ret 指令) ?

2 个答案:

答案 0 :(得分:2)

  

我的问题是:标准中是否有保证可以保证对于std::movestd::forward之类的函数(仅能进行强制转换),必须始终对其进行内联(因此,不存在任何机器代码)生成)?

不。该标准停止描述抽象机的可观察行为。代码生成是标准不了解的实现细节。

话虽这么说,std::forwardstd::move都是只真正影响C ++类型系统的操作,而不影响实际数据,所以我非常惊讶地看到为它们生成的任何机器代码优化版本

另一方面,在未优化的构建中,保留std::move的轮廓(几乎与其他任何函数调用一样)可能是简化调试的一个好主意。您可以轻松地对此进行测试(live on gcc.godbolt):

#include <utility>

struct Foo {
    int i;
    Foo(Foo &&other) :i(other.i) {};
};

Foo with_move(Foo f) {
    return std::move(f);
}

在带有-O0 std::move的gcc中,它是作为实际函数生成的(除了设置/删除堆栈帧并返回接收到的指针参数外,什么也没做)

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

答案 1 :(得分:0)

这不是来自标准,而是来自Scott Meyer的有效CPP。摘录
第30项:了解内联的来龙去脉。

  

编译器优化是   通常是为缺少函数调用的代码段而设计的,因此   内联函数时,可以使编译器执行上下文-   功能主体上的特定优化。大多数编译器   永远不要在“概述”的函数调用上执行此类优化。
  ...
  在内存有限的机器上,过于热情   内联会导致程序太大而无法使用   空间。即使有了虚拟内存,内联导致的代码膨胀也会导致   进行额外的分页,降低指令高速缓存命中率以及   这些事情伴随着性能损失。
  ...
  另一方面,如果内联函数体非常短,则代码   为函数主体生成的代码可能小于生成的代码   用于函数调用。在这种情况下,内联函数可能会   实际上导致较小的目标代码和较高的指令缓存命中率   率!
  ...
  请记住,内联是对编译器的请求,而不是命令。   ...
  模板实例化与内联无关。如果您正在撰写   模板,并且您相信从   应该内联模板,声明内联模板;
  ...
  但是,如果您为不需要内联的函数编写模板,请避免声明内联模板(显式或隐式)。内联   有成本,而您不想在没有深思熟虑的情况下产生成本。
  ...
  ... 让我们观察一下,内联是一个请求   编译器可能会忽略。大多数编译器拒绝内联函数   它们认为过于复杂(例如,包含循环或递归的循环),   除了最琐碎的虚函数调用之外,其他所有函数都无法内联

     

所有这些加起来:给定的内联函数是否实际上是   内联取决于您使用的构建环境,主要是   编译器。幸运的是,大多数编译器的诊断级别   如果他们无法内联函数,将导致警告   您已要求他们这样做。

     

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

如果您可以掌握这本书,请阅读整本书,这将消除您的许多疑问。