Как шаблон extern генерирует код? - PullRequest
1 голос
/ 30 марта 2019

Я понимаю цель явной реализации шаблона вместе с синтаксисом extern template. Идея состоит в том, чтобы гарантировать, что определенный шаблон создается только в одной единице перевода, что потенциально сокращает время компиляции.

Но я не совсем понимаю, как это работает на практике. extern template указывает компилятору не неявно создавать экземпляр шаблона в текущем модуле перевода с обещанием, что он найдет необходимый шаблон, явно созданный в каком-то другом модуле перевода во время ссылки. Но как компилятору избежать создания экземпляра шаблона в это время?

Например, скажем, у нас есть:

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

extern template class Foo<int>;

Теперь допустим, что в том же переводе мы делаем что-то вроде:

Foo<int> f;
// now do lots of stuff with f

На данный момент, из-за оператора extern template class, компилятору не разрешено неявно создавать экземпляр Foo<int>. Но тогда как он может генерировать какой-либо код для текущего блока перевода? Нужно создать экземпляр Foo<int>, чтобы узнать даже размер Foo<int>, чтобы даже знать, как двигаться вверх по указателю стека, когда в стеке объявлено Foo<int> f.

Кроме того, Foo<int> может иметь всевозможные вложенные определения типов или различные функции-члены, зависящие от типа T, которые невозможно было бы скомпилировать без неявного создания экземпляра Foo<int>.

Так как это работает? Компилятор просто не генерирует какой-либо код, включающий Foo<int> при компиляции этого модуля перевода? А затем позже, на этапе компоновки, он возвращается и объединяет необходимый код в объектный файл после того, как обнаруживает явное создание экземпляра Foo<int> в каком-то другом модуле перевода?

Если это так, не означает ли это, что побочным эффектом использования extern template может быть потенциально увеличенное время компоновщика, поскольку большая часть генерации кода должна происходить во время компоновки, а не во время компиляции?

1 Ответ

2 голосов
/ 30 марта 2019

Это на самом деле довольно просто. Вот заголовочный файл, который определяет шаблон класса 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 просто называет группу разделов, в которой компилятор помещает раздел функций, в котором определен конструктор.

...