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