Это на самом деле довольно просто. Вот заголовочный файл, который определяет шаблон класса foo<T>
:
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
Вот исходный файл, который явно создает определение класса foo<int>
:
foo_int.cpp
#include "foo.hpp"
// An explicit instantiation definition
template struct foo<int>;
Когда мы скомпилируем foo_int.cpp
в foo_int.o
, этот объектный файл определит все
символы, получаемые от создания foo<int>
:
$ g++ -Wall -Wextra -pedantic -c foo_int.cpp
$ nm --defined-only foo_int.o
0000000000000000 W _ZN3fooIiE3setERKi
0000000000000000 W _ZNK3fooIiE3getEv
, что с устранением искажения:
$ nm -C --defined-only foo_int.o
0000000000000000 W foo<int>::set(int const&)
0000000000000000 W foo<int>::get() const
(Обратите внимание, что символы определены слабо
- W
- так же, как они будут в результате неявной реализации. Обратите внимание, что
компилятор не видел необходимости вообще создавать какие-либо определения для любого из неявно дефолтованных
специальные функции-члены.)
Вот заголовочный файл, который объявляет явное создание foo<int>
такого
как мы только что определили в foo_int.o
:
foo_int.hpp
#ifndef FOO_INT_HPP
#define FOO_INT_HPP
#include "foo.hpp"
// An explicit instantiation declaration
extern template struct foo<int>;
#endif
Вот исходный файл, который ссылается на явную реализацию foo<int>
что мы объявили в foo_int.hpp
:
make_foo_int.cpp
#include "make_foo_int.hpp"
foo<int> make_foo_int(int i)
{
foo<int> fi;
fi.set(i);
return fi;
}
и связанный с ним заголовочный файл:
make_foo_int.hpp
#ifndef MAKE_FOO_INT_HPP
#define MAKE_FOO_INT_HPP
#include "foo_int.hpp"
foo<int> make_foo_int(int i = 0);
#endif
Обратите внимание, что make_foo_int.cpp
- это переводчик, который озадачивает
вы. Это #include
с make_foo_int.hpp
, что #include
с foo_int.hpp
,
который #include
с foo.hpp
- определение шаблона. И тогда это "делает вещи" с
foo<int>
.
Когда мы компилируем make_foo_int.cpp
в make_foo_int.o
, этот объектный файл
будет содержать только неопределенные ссылки на любые символы, которые получены из
создание foo<int>
:
$ g++ -Wall -Wextra -pedantic -c make_foo_int.cpp
$ nm -C --defined-only make_foo_int.o
0000000000000000 T make_foo_int(int)
$ nm -C --undefined-only make_foo_int.o
U _GLOBAL_OFFSET_TABLE_
U __stack_chk_fail
U foo<int>::set(int const&)
Компилятор просто не генерирует какой-либо код, включающий Foo<int>
при компиляции этого модуля перевода?
Компилятор генерирует вызов для неопределенной внешней функции foo<int>::set(int const&)
. Вот
сборка:
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<int>::set(int const&)
через таблицу поиска процедур,
так же, как он может генерировать вызов любой неопределенной внешней функции, которая должна быть
разрешается в linktime .
Теперь вот исходный файл для программы, которая вызывает make_foo_int
, а также foo<int>::get
:
main.cpp
#include "make_foo_int.hpp"
#include <iostream>
int main()
{
std::cout << make_foo_int(42).get() << std::endl;
return 0;
}
Если мы скомпилируем main.cpp
, объектный файл также будет содержать только неопределенные ссылки
к символам, которые происходят от реализации foo<int>
:
$ g++ -Wall -Wextra -pedantic -c main.cpp
$ nm -C --defined-only main.o | grep foo; echo Done
Done
$ nm -C --undefined-only main.o | grep foo; echo Done
U make_foo_int(int)
U foo<int>::get() const
Done
Если мы попытаемся связать программу, используя main.o
и make_foo_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
происходит сбой с неопределенными ссылками на foo<int>::get()
и foo<int>::set(int const&)
.
Если мы перешли с добавлением необходимого foo_int.o
и попросим компоновщика
сообщить ссылки и определения этих символов:
$ g++ -o prog main.o make_foo_int.o foo_int.o -Wl,-trace-symbol=_ZN3fooIiE3setERKi,-trace-symbol=_ZNK3fooIiE3getEv
/usr/bin/ld: main.o: reference to _ZNK3fooIiE3getEv
/usr/bin/ld: make_foo_int.o: reference to _ZN3fooIiE3setERKi
/usr/bin/ld: foo_int.o: definition of _ZNK3fooIiE3getEv
/usr/bin/ld: foo_int.o: definition of _ZN3fooIiE3setERKi
мы добились успеха и видим, что компоновщик находит ссылку на foo<int>::get()
в main.o
,
ссылка на foo<int>::set(int const&)
в make_foo_int.o
и
определения обоих символов в foo_int.o
. foo<int>
был создан экземпляр
только один раз, в foo_int.o
.
Позже ...
Согласно вашим комментариям, вы все еще не видите, как может быть make_foo_int(int)
функция
скомпилирован без компилятора foo<int>
если только для цели
вычисления размера, который автоматический объект foo<int> fi
, который определен
в функции будет занимать в стеке.
Чтобы лучше решить эту проблему, мне сначала нужно нарисовать точку, которой, вероятно, было недостаточно
Ясно, прежде чем я заметил, что явный экземпляр:
template struct foo<int>;
в foo_int.cpp
генерирует только определения функций-членов , которые определены
по шаблону , как показано:
1161 * *
и не генерирует определения неявно дефолтных специальных членов
класс - конструкторы и пр.
Итак, проблема, очень похожая на вашу, заключается в следующем: как можно скомпилировать функцию make_foo_int(int)
без создания компилятором хотя бы конструктора по умолчанию, который
исполнено:
foo<int> fi;
? Ответ таков: он создает экземпляр этого конструктора, встроенный, как обычно.
(По крайней мере, так будет, если конструктор не запрещен). Но это только так , потому что
мы не определили этот конструктор в шаблоне, который мы явно создали
в foo_int.cpp
.
Давайте тоже немного изменим шаблон:
foo.hpp (2)
#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 = 257; // <- Default initializer
};
#endif
Затем перекомпилируйте make_foo_int.cpp
, сохранив сборку:
$ g++ -Wall -Wextra -pedantic -c make_foo_int.cpp -save-temps
, что теперь делает очевидным, что конструктор по умолчанию foo<int>()
встраивается, тогда как foo<int>::set(T const &)
вызывается извне:
make_foo_int.s (2)
.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
movl $257, -12(%rbp) ; <- Default initializer
leaq -20(%rbp), %rdx
leaq -12(%rbp), %rax
movq %rdx, %rsi
movq %rax, %rdi
call _ZN3fooIiE3setERKi@PLT ; <- External call
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
Компилятор способен вставлять, как обычно, любые специальные функции-члены
foo<int>
что мы не определили в шаблоне, потому что шаблон
определение должно быть доступно ему всякий раз, когда оно видит:
extern template struct foo<int>;
как мы можем проверить, изменив foo_int.hpp
на:
foo_int.hpp (2)
#ifndef FOO_INT_HPP
#define FOO_INT_HPP
//#include "foo.hpp" <- Hide the template definition
template <typename T> struct foo;
// An explicit instantiation declaration
extern template struct foo<int>;
#endif
и попытка:
$ 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<int>
"
в make_foo_int.o
. Но это только создание экземпляра части - конструктора по умолчанию - что
не предоставляется в качестве внешней ссылки:
extern template struct foo<int>;
и этот конструктор по умолчанию не предусмотрен, потому что мы не определили его в
template struct foo<T>
.
Если мы делаем определяем конструкторы в шаблоне, скажем:
foo.hpp (3)
#ifndef FOO_HPP
#define FOO_HPP
template<typename T>
struct foo
{
foo()
: _t{257}{}
foo(foo const & other)
: _t{other._t}{}
T const & get() const {
return _t;
}
void set(T const & t) {
_t = t;
}
private:
T _t;
};
#endif
тогда мы найдем их, определенные в foo_int.o
:
$ 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
(похоже, они умножены определены, но это иллюзия и отвлечение! 1 ). И если мы
перекомпилируйте make_foo_int.cpp
с foo.hpp
3 и нашим оригинальным foo_int.hpp
:
и осмотрите новую сборку:
$ 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
теперь мы видим, что конструктор по умолчанию _ZN3fooIiEC1E
также
как set
функция-член _ZN3fooIiE3setERKi
вызывается извне.
Перезапуская нашу оригинальную программу, она запускается:
$ g++ -Wall -Wextra -pedantic -O0 -o prog main.cpp make_foo_int.cpp foo_int.cpp
$ ./prog
42
Что в конечном итоге подготавливает нас к вопросу: как компилятор может узнать размер
объекта foo<int> fi
для того, чтобы скомпилировать функцию make_foo_int
, без
создание экземпляров foo<int>
?
Как показывает make_foo_int.s.before
, компилятору не нужно вычислять размер
любого такого объекта, потому что в генерируемом коде такого объекта не существует. C ++
классы и экземпляры классов неизвестны в сборке и объектном коде. В объекте
код, есть только функции и объекты фундаментальной интегральной или с плавающей запятой
типы, размеры которых все известны с самого начала. Функция выполняется с 0 или более аргументами; возможно, действует на объекты
из этих основных типов, находящихся в стеке, куче или статической памяти, и
он (как правило) возвращает управление предшествующему контексту. Заявление C ++:
foo<int> fi;
в теле make_foo_int
буквально не компилируется для размещения объекта
fi
в стеке. Компилируется для выполнения функции, которая является конструктором по умолчанию
foo<int>
- возможно встроенный, возможно вызванный извне; не имеет значения - какие места
целое число = 257 в его стеке и заканчивается, оставляя это целое число в стеке для
его звонящий. Вызывающему абоненту, как всегда, не нужно знать чистое потребление стека вызываемым абонентом.
Мы могли бы переопределить template struct foo<T>
(довольно безумно), что делает foo<int>
1000
раз больше:
foo.hpp (4)
#ifndef FOO_HPP
#define FOO_HPP
template<typename T>
struct foo
{
foo() {
for (unsigned i = 0; i < 1000; ++i) {
_t[i] = 257;
}
}
foo(foo const & other) {
for (unsigned i = 0; i < 1000; ++i) {
_t[i] = other._t[i];
}
}
T const & get() const {
return _t[999];
}
void set(T const & t) {
_t[0] = t;
}
private:
T _t[1000];
};
#endif
затем перекомпилировать make_foo_int.cpp
:
$ 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.o
:
$ 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
хотя это имеет значение для нашей программы:
$ g++ -Wall -Wextra -pedantic -O0 -o prog main.cpp make_foo_int.cpp foo_int.cpp
$ ./prog
257
Я с готовностью отрекаюсь от своего вступительного комментария, что "Это на самом деле довольно просто":)
[1] Выход:
$ 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
, кажется, говорит, что каждый из конструкторов имеет два слабо глобальных определения
и дополнительно определяется как символ comdat
! Но если мы отключим демангл
этот вид исчезает:
$ 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
и мы видим, что все символы в действительности различны. ABI калечат
карты всех трех из:
_ZN3fooIiEC1ERKS0_
_ZN3fooIiEC2ERKS0_
_ZN3fooIiEC5ERKS0_
до foo<int>::foo(foo<int> const&)
, а также все:
_ZN3fooIiEC1Ev
_ZN3fooIiEC2Ev
_ZN3fooIiEC5Ev
до foo<int>::foo()
. В GCC рецепт для компиляции этих конструкторов,
варианты символов, содержащие C1
и C2
, являются символами, которые на самом деле
эквивалентны, но логически различаются в спецификации ABI , а
вариант с C5
просто называет группу разделов, в которой компилятор
помещает раздел функций, в котором определен конструктор.