C++ extern模板实际上是如何生成代码的?

C++ extern模板实际上是如何生成代码的?,c++,templates,linker,C++,Templates,Linker,我理解的目的以及外部模板的语法。其思想是确保某个模板只在一个翻译单元中实例化,从而潜在地减少编译时间 但我不知道这在实践中是如何起作用的。extern模板指示编译器不要在当前翻译单元中隐式实例化模板,并保证它将在链接时找到在其他翻译单元中显式实例化的必要模板。但是编译器如何避免同时实例化模板呢 例如,假设我们有: template <class T> struct Foo { T value; }; extern template class Foo<int>; 模板

我理解的目的以及
外部模板的语法。其思想是确保某个模板只在一个翻译单元中实例化,从而潜在地减少编译时间

但我不知道这在实践中是如何起作用的。
extern模板
指示编译器不要在当前翻译单元中隐式实例化模板,并保证它将在链接时找到在其他翻译单元中显式实例化的必要模板。但是编译器如何避免同时实例化模板呢

例如,假设我们有:

template <class T>
struct Foo { T value; };

extern template class Foo<int>;
模板
结构Foo{T value;};
外部模板类Foo;
现在,让我们假设在同一个翻译单元中,我们执行以下操作:

Foo<int> f;
// now do lots of stuff with f
foof;
//现在用f做很多事情
此时,由于
extern template class
语句,编译器不允许隐式实例化
Foo
。但是它怎么能为当前的翻译单元生成代码呢?它需要实例化
Foo
以了解
Foo
的大小,以便知道在堆栈上声明
Foo f
时向上移动堆栈指针的量

另外,
Foo
可能有各种嵌套的typedef,或者依赖于类型
T
的各种成员函数,如果不隐式实例化
Foo
,就无法编译这些函数

那么这是如何工作的呢?编译此翻译单元时,编译器是否只是不生成任何涉及
Foo
的代码?然后在链接器阶段,在其他翻译单元中找到
Foo
的显式实例化后,它是否返回并将必要的代码拼接到目标文件中


如果是这样,这是否也意味着使用
外部模板的副作用可能会增加链接器的次数,因为大量代码生成必须在链接时而不是在编译时进行?

这实际上非常简单。下面是一个头文件,用于定义模板类
foo

foo.hpp

#ifndef FOO_HPP
#define FOO_HPP

template<typename T>
struct foo
{
    T const & get() const {
        return _t;
    }
    void set(T const & t) {
        _t = t;
    }

private:
    T _t;
}

#endif
#include "foo.hpp"

// An explicit instantiation definition
template struct foo<int>;
#ifndef FOO_INT_HPP
#define FOO_INT_HPP

#include "foo.hpp"

// An explicit instantiation declaration
extern template struct foo<int>;

#endif
#include "make_foo_int.hpp"

foo<int> make_foo_int(int i)
{
    foo<int> fi;
    fi.set(i);
    return fi;
}
#ifndef MAKE_FOO_INT_HPP
#define MAKE_FOO_INT_HPP
#include "foo_int.hpp"

foo<int> make_foo_int(int i = 0);

#endif
#include "make_foo_int.hpp"
#include <iostream>

int main()
{
    std::cout << make_foo_int(42).get() << std::endl;
    return 0;
}
其中,带去碎片的是:

$ nm -C --defined-only foo_int.o
0000000000000000 W foo<int>::set(int const&)
0000000000000000 W foo<int>::get() const
下面是一个源文件,它引用了
foo的显式实例化
我们在
foo_int.hpp
中声明:

$ g++ -Wall -Wextra -pedantic -O0 -c make_foo_int.cpp -save-temps
$ mv make_foo_int.s make_foo_int.s.after
make_foo_int.cpp

#ifndef FOO_HPP
#define FOO_HPP

template<typename T>
struct foo
{
    T const & get() const {
        return _t;
    }
    void set(T const & t) {
        _t = t;
    }

private:
    T _t;
}

#endif
#include "foo.hpp"

// An explicit instantiation definition
template struct foo<int>;
#ifndef FOO_INT_HPP
#define FOO_INT_HPP

#include "foo.hpp"

// An explicit instantiation declaration
extern template struct foo<int>;

#endif
#include "make_foo_int.hpp"

foo<int> make_foo_int(int i)
{
    foo<int> fi;
    fi.set(i);
    return fi;
}
#ifndef MAKE_FOO_INT_HPP
#define MAKE_FOO_INT_HPP
#include "foo_int.hpp"

foo<int> make_foo_int(int i = 0);

#endif
#include "make_foo_int.hpp"
#include <iostream>

int main()
{
    std::cout << make_foo_int(42).get() << std::endl;
    return 0;
}
编译此翻译单元时,编译器是否只是不生成任何涉及
Foo
的代码

编译器生成对未定义的外部函数
foo::set(int const&)
的调用。这是 大会:

$ g++ -Wall -Wextra -pedantic -c make_foo_int.cpp -save-temps
$ g++ -Wall -Wextra -pedantic -O0 -c make_foo_int.cpp -save-temps
$ mv make_foo_int.s make_foo_int.s.before   # Save that for later
$ cat make_foo_int.s.before
    .file   "make_foo_int.cpp"
    .text
    .globl  _Z12make_foo_inti
    .type   _Z12make_foo_inti, @function
_Z12make_foo_inti:
.LFB4:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    subq    $32, %rsp
    movq    %rdi, -24(%rbp)
    movl    %esi, -28(%rbp)
    movq    %fs:40, %rax
    movq    %rax, -8(%rbp)
    xorl    %eax, %eax
    movq    -24(%rbp), %rax
    movq    %rax, %rdi
    call    _ZN3fooIiEC1Ev@PLT      ; <- External ctor call
    leaq    -28(%rbp), %rdx
    movq    -24(%rbp), %rax
    movq    %rdx, %rsi
    movq    %rax, %rdi
    call    _ZN3fooIiE3setERKi@PLT  ; <- External `set` call
    nop
    movq    -24(%rbp), %rax
    movq    -8(%rbp), %rcx
    xorq    %fs:40, %rcx
    je  .L3
    call    __stack_chk_fail@PLT
.L3:
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE4:
    .size   _Z12make_foo_inti, .-_Z12make_foo_inti
    .ident  "GCC: (Ubuntu 8.2.0-7ubuntu1) 8.2.0"
    .section    .note.GNU-stack,"",@progbits
make_foo_int.s

    .file   "make_foo_int.cpp"
    .text
    .globl  _Z12make_foo_inti
    .type   _Z12make_foo_inti, @function
_Z12make_foo_inti:
.LFB2:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    subq    $32, %rsp
    movl    %edi, -20(%rbp)
    movq    %fs:40, %rax
    movq    %rax, -8(%rbp)
    xorl    %eax, %eax
    leaq    -20(%rbp), %rdx
    leaq    -12(%rbp), %rax
    movq    %rdx, %rsi
    movq    %rax, %rdi
    call    _ZN3fooIiE3setERKi@PLT
    movl    -12(%rbp), %eax
    movq    -8(%rbp), %rcx
    xorq    %fs:40, %rcx
    je  .L3
    call    __stack_chk_fail@PLT
.L3:
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE2:
    .size   _Z12make_foo_inti, .-_Z12make_foo_inti
    .ident  "GCC: (Ubuntu 8.2.0-7ubuntu1) 8.2.0"
    .section    .note.GNU-stack,"",@progbits
int其中:

call    _ZN3fooIiE3setERKi@PLT
是通过过程查找表对
foo::set(int const&)
的调用, 就像它可能会生成对任何未定义的外部函数的调用一样 在链接时间解决

下面是一个程序的源文件,该程序调用
make\u foo\u int
foo::get

main.cpp

#ifndef FOO_HPP
#define FOO_HPP

template<typename T>
struct foo
{
    T const & get() const {
        return _t;
    }
    void set(T const & t) {
        _t = t;
    }

private:
    T _t;
}

#endif
#include "foo.hpp"

// An explicit instantiation definition
template struct foo<int>;
#ifndef FOO_INT_HPP
#define FOO_INT_HPP

#include "foo.hpp"

// An explicit instantiation declaration
extern template struct foo<int>;

#endif
#include "make_foo_int.hpp"

foo<int> make_foo_int(int i)
{
    foo<int> fi;
    fi.set(i);
    return fi;
}
#ifndef MAKE_FOO_INT_HPP
#define MAKE_FOO_INT_HPP
#include "foo_int.hpp"

foo<int> make_foo_int(int i = 0);

#endif
#include "make_foo_int.hpp"
#include <iostream>

int main()
{
    std::cout << make_foo_int(42).get() << std::endl;
    return 0;
}
如果我们尝试仅使用
main.o
make\u foo\u int.o
链接程序:

$ g++ -o prog main.o make_foo_int.o
/usr/bin/ld: main.o: in function `main':
main.cpp:(.text+0x2c): undefined reference to `foo<int>::get() const'
/usr/bin/ld: make_foo_int.o: in function `make_foo_int(int)':
make_foo_int.cpp:(.text+0x29): undefined reference to `foo<int>::set(int const&)'
collect2: error: ld returned 1 exit status
$ g++ -Wall -Wextra -pedantic -c foo_int.cpp
$ nm -C foo_int.o
0000000000000000 W foo<int>::set(int const&)
0000000000000000 W foo<int>::foo(foo<int> const&)
0000000000000000 W foo<int>::foo()
0000000000000000 W foo<int>::foo(foo<int> const&)
0000000000000000 W foo<int>::foo()
0000000000000000 n foo<int>::foo(foo<int> const&)
0000000000000000 n foo<int>::foo()
0000000000000000 W foo<int>::get() const
$ diff make_foo_int.s.before make_foo_int.s.after; echo Done
Done

$ cat make_foo_int.s.after
    .file   "make_foo_int.cpp"
    .text
    .globl  _Z12make_foo_inti
    .type   _Z12make_foo_inti, @function
_Z12make_foo_inti:
.LFB4:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    subq    $32, %rsp
    movq    %rdi, -24(%rbp)
    movl    %esi, -28(%rbp)
    movq    %fs:40, %rax
    movq    %rax, -8(%rbp)
    xorl    %eax, %eax
    movq    -24(%rbp), %rax
    movq    %rax, %rdi
    call    _ZN3fooIiEC1Ev@PLT
    leaq    -28(%rbp), %rdx
    movq    -24(%rbp), %rax
    movq    %rdx, %rsi
    movq    %rax, %rdi
    call    _ZN3fooIiE3setERKi@PLT
    nop
    movq    -24(%rbp), %rax
    movq    -8(%rbp), %rcx
    xorq    %fs:40, %rcx
    je  .L3
    call    __stack_chk_fail@PLT
.L3:
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE4:
    .size   _Z12make_foo_inti, .-_Z12make_foo_inti
    .ident  "GCC: (Ubuntu 8.2.0-7ubuntu1) 8.2.0"
    .section    .note.GNU-stack,"",@progbits
我们成功了,并且看到链接器在
main.o
中找到了对
foo::get()
的引用, 对
make\u foo\u int.o
中的
foo::set(int const&)
的引用,以及
foo_int.o
中两个符号的定义<已实例化代码>foo
只有一次,在
foo_int.o

稍后…

根据您的评论,您仍然看不到函数
make\u foo\u int(int)
是如何实现的 编译时没有编译器实例化
foo
,如果只是为了 计算定义的自动对象
foo-fi
的大小 在函数中,将占用堆栈上的空间

为了更好地解决这个问题,我首先需要指出一个可能不够充分的观点 当我注意到显式实例化:

template struct foo<int>;
并且不会生成的隐式默认特殊成员的定义 类-构造函数等

因此,一个与您非常相似的问题是:如果编译器不至少实例化默认构造函数,如何编译函数
使_foo_int(int)
执行人:

foo<int> fi;
然后重新编译
make_foo_int.cpp
,保存程序集:

$ g++ -Wall -Wextra -pedantic -c make_foo_int.cpp -save-temps
$ g++ -Wall -Wextra -pedantic -O0 -c make_foo_int.cpp -save-temps
$ mv make_foo_int.s make_foo_int.s.before   # Save that for later
$ cat make_foo_int.s.before
    .file   "make_foo_int.cpp"
    .text
    .globl  _Z12make_foo_inti
    .type   _Z12make_foo_inti, @function
_Z12make_foo_inti:
.LFB4:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    subq    $32, %rsp
    movq    %rdi, -24(%rbp)
    movl    %esi, -28(%rbp)
    movq    %fs:40, %rax
    movq    %rax, -8(%rbp)
    xorl    %eax, %eax
    movq    -24(%rbp), %rax
    movq    %rax, %rdi
    call    _ZN3fooIiEC1Ev@PLT      ; <- External ctor call
    leaq    -28(%rbp), %rdx
    movq    -24(%rbp), %rax
    movq    %rdx, %rsi
    movq    %rax, %rdi
    call    _ZN3fooIiE3setERKi@PLT  ; <- External `set` call
    nop
    movq    -24(%rbp), %rax
    movq    -8(%rbp), %rcx
    xorq    %fs:40, %rcx
    je  .L3
    call    __stack_chk_fail@PLT
.L3:
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE4:
    .size   _Z12make_foo_inti, .-_Z12make_foo_inti
    .ident  "GCC: (Ubuntu 8.2.0-7ubuntu1) 8.2.0"
    .section    .note.GNU-stack,"",@progbits
现在很明显,默认构造函数
foo()
是内联的,而
foo::set(T const&)
是外部调用的:

make\u foo\u int.s(2)

并尝试:

$ g++ -Wall -Wextra -pedantic -c make_foo_int.cpp -save-temps
In file included from make_foo_int.hpp:3,
                 from make_foo_int.cpp:1:
foo_int.hpp:9:24: error: explicit instantiation of ‘struct foo<int>’ before definition of template
 extern template struct foo<int>;
                        ^~~~~~~~
默认构造函数不是这样提供的,因为我们没有在中定义它
模板结构foo

如果我们在模板中定义了构造函数,请说:

foo.hpp(3)

(看起来它们是多重定义的,但这是一种幻觉和分心!1)。如果我们 使用
foo.hpp
3和我们原来的
foo.hpp
重新编译
make_foo\u int.cpp
: 并检查新总成:

$ g++ -Wall -Wextra -pedantic -c make_foo_int.cpp -save-temps
$ g++ -Wall -Wextra -pedantic -O0 -c make_foo_int.cpp -save-temps
$ mv make_foo_int.s make_foo_int.s.before   # Save that for later
$ cat make_foo_int.s.before
    .file   "make_foo_int.cpp"
    .text
    .globl  _Z12make_foo_inti
    .type   _Z12make_foo_inti, @function
_Z12make_foo_inti:
.LFB4:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    subq    $32, %rsp
    movq    %rdi, -24(%rbp)
    movl    %esi, -28(%rbp)
    movq    %fs:40, %rax
    movq    %rax, -8(%rbp)
    xorl    %eax, %eax
    movq    -24(%rbp), %rax
    movq    %rax, %rdi
    call    _ZN3fooIiEC1Ev@PLT      ; <- External ctor call
    leaq    -28(%rbp), %rdx
    movq    -24(%rbp), %rax
    movq    %rdx, %rsi
    movq    %rax, %rdi
    call    _ZN3fooIiE3setERKi@PLT  ; <- External `set` call
    nop
    movq    -24(%rbp), %rax
    movq    -8(%rbp), %rcx
    xorq    %fs:40, %rcx
    je  .L3
    call    __stack_chk_fail@PLT
.L3:
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE4:
    .size   _Z12make_foo_inti, .-_Z12make_foo_inti
    .ident  "GCC: (Ubuntu 8.2.0-7ubuntu1) 8.2.0"
    .section    .note.GNU-stack,"",@progbits
这最终为我们提出了一个问题:编译器如何知道大小 对象的
foo-fi
,以编译函数make_-foo\u-int,而无需 实例化
foo

正如
make_foo_int.s.before
所表明的那样,编译器不需要计算大小 因为在它生成的代码中,不存在这样的对象。C++ 类和类的实例在程序集和对象代码中是未知的。反对 代码中,只有基本积分或浮点的函数和对象 从一开始就知道其大小的类型。执行一个具有0个或多个参数的函数;它可能作用在物体上 位于堆栈、堆或静态存储中的基本类型,以及 它(通常)返回con
$ nm foo_int.o
0000000000000000 W _ZN3fooIiE3setERKi
0000000000000000 W _ZN3fooIiEC1ERKS0_
0000000000000000 W _ZN3fooIiEC1Ev
0000000000000000 W _ZN3fooIiEC2ERKS0_
0000000000000000 W _ZN3fooIiEC2Ev
0000000000000000 n _ZN3fooIiEC5ERKS0_
0000000000000000 n _ZN3fooIiEC5Ev
0000000000000000 W _ZNK3fooIiE3getEv
_ZN3fooIiEC1ERKS0_
_ZN3fooIiEC2ERKS0_
_ZN3fooIiEC5ERKS0_
_ZN3fooIiEC1Ev
_ZN3fooIiEC2Ev
_ZN3fooIiEC5Ev