Двойное выделение символов конструктора

Сегодня я обнаружил довольно интересную вещь о g++ или nm... определения конструктора, по-видимому, имеют две записи в библиотеках.

У меня есть заголовок thing.hpp:

class Thing
{
    Thing();

    Thing(int x);

    void foo();
};

И thing.cpp:

#include "thing.hpp"

Thing::Thing()
{ }

Thing::Thing(int x)
{ }

void Thing::foo()
{ }

Я скомпилирую это с помощью:

g++ thing.cpp -c -o libthing.a

Затем я запустил на нем nm:

%> nm -gC libthing.a
0000000000000030 T Thing::foo()
0000000000000022 T Thing::Thing(int)
000000000000000a T Thing::Thing()
0000000000000014 T Thing::Thing(int)
0000000000000000 T Thing::Thing()
                 U __gxx_personality_v0

Как вы можете видеть, оба конструктора для Thing перечислены с двумя записями в сгенерированной статической библиотеке. Мой g++ равен 4.4.3, но такое же поведение происходит в clang, поэтому это проблема не только gcc.

Это не вызывает никаких очевидных проблем, но мне было интересно:

  • Почему определенные конструкторы перечислены дважды?
  • Почему это не вызывает проблемы с "множественным определением символа __"?

EDIT: для Carl вывод без аргумента C:

%> nm -g libthing.a
0000000000000030 T _ZN5Thing3fooEv
0000000000000022 T _ZN5ThingC1Ei
000000000000000a T _ZN5ThingC1Ev
0000000000000014 T _ZN5ThingC2Ei
0000000000000000 T _ZN5ThingC2Ev
                 U __gxx_personality_v0

Как вы можете видеть... одна и та же функция генерирует несколько символов, что все еще довольно любопытно.

И пока мы на нем, вот раздел сгенерированной сборки:

.globl _ZN5ThingC2Ev
        .type   _ZN5ThingC2Ev, @function
_ZN5ThingC2Ev:
.LFB1:
        .cfi_startproc
        .cfi_personality 0x3,__gxx_personality_v0
        pushq   %rbp
        .cfi_def_cfa_offset 16
        movq    %rsp, %rbp
        .cfi_offset 6, -16
        .cfi_def_cfa_register 6
        movq    %rdi, -8(%rbp)
        leave
        ret
        .cfi_endproc
.LFE1:
        .size   _ZN5ThingC2Ev, .-_ZN5ThingC2Ev
        .align 2
.globl _ZN5ThingC1Ev
        .type   _ZN5ThingC1Ev, @function
_ZN5ThingC1Ev:
.LFB2:
        .cfi_startproc
        .cfi_personality 0x3,__gxx_personality_v0
        pushq   %rbp
        .cfi_def_cfa_offset 16
        movq    %rsp, %rbp
        .cfi_offset 6, -16
        .cfi_def_cfa_register 6
        movq    %rdi, -8(%rbp)
        leave
        ret
        .cfi_endproc

Итак, сгенерированный код... ну... тот же.


EDIT. Чтобы узнать, какой конструктор действительно вызван, я изменил Thing::foo() на это:

void Thing::foo()
{
    Thing t;
}

Сгенерированная сборка:

.globl _ZN5Thing3fooEv
        .type   _ZN5Thing3fooEv, @function
_ZN5Thing3fooEv:
.LFB550:
        .cfi_startproc
        .cfi_personality 0x3,__gxx_personality_v0
        pushq   %rbp
        .cfi_def_cfa_offset 16
        movq    %rsp, %rbp
        .cfi_offset 6, -16
        .cfi_def_cfa_register 6
        subq    $48, %rsp
        movq    %rdi, -40(%rbp)
        leaq    -32(%rbp), %rax
        movq    %rax, %rdi
        call    _ZN5ThingC1Ev
        leaq    -32(%rbp), %rax
        movq    %rax, %rdi
        call    _ZN5ThingD1Ev
        leave
        ret
        .cfi_endproc

Таким образом, он вызывает полный конструктор объекта.

Ответ 1

Начнем с объявления, что GCC следует Itanium С++ ABI.


В соответствии с ABI, искаженное имя для вашего Thing::foo() легко анализируется:

_Z     | N      | 5Thing  | 3foo | E          | v
prefix | nested | `Thing` | `foo`| end nested | parameters: `void`

Вы также можете прочитать имена конструкторов, как показано ниже. Обратите внимание, что конструктор "name" не указан, но вместо этого: C:

_Z     | N      | 5Thing  | C1          | E          | i
prefix | nested | `Thing` | Constructor | end nested | parameters: `int`

Но что это за C1? Ваш дубликат имеет C2. Что это значит?

Ну, это тоже довольно просто:

  <ctor-dtor-name> ::= C1   # complete object constructor
                   ::= C2   # base object constructor
                   ::= C3   # complete object allocating constructor
                   ::= D0   # deleting destructor
                   ::= D1   # complete object destructor
                   ::= D2   # base object destructor

Подождите, почему это так просто? Этот класс не имеет базы. Почему у него есть "конструктор объектов" и "конструктор базового объекта" для каждого?

  • Этот Q & A подразумевает, что это просто побочный продукт поддержки полиморфизма, хотя в этом случае он фактически не требуется.

  • Обратите внимание, что c++filt используется для включения этой информации в ее демонтированный вывод, но больше не существует.

  • Этот пост форума задает один и тот же вопрос, и единственный ответ не помогает лучше ответить на него, кроме что GCC может избегать испускания двух конструкторов, когда полиморфизм не задействован, и что это поведение должно быть улучшено в будущем.

  • Эта публикация в группе новостей описывает проблему с установкой точек останова в конструкторах из-за этого двойного излучения. Он снова заявил, что корень проблемы - поддержка полиморфизма.

Фактически это указано как "известная проблема" GCC:

g++ испускает две копии конструкторов и деструкторов.

В общем случае существуют три типа конструкторов (и деструкторы).

  • Полный конструктор/деструктор объекта.
  • Конструктор/деструктор базового объекта.
  • Назначающий конструктор/деаллоцирующий деструктор.

Первые два отличаются друг от друга, когда виртуальные базовые классы участие.


Значение этих разных конструкторов выглядит следующим образом:

  • "Полный конструктор объектов". Он дополнительно создает виртуальные базовые классы.

  • "Конструктор базового объекта". Он создает сам объект, а также элементы данных и не виртуальные базовые классы.

  • "Назначение конструктора объектов". Он делает все, что делает полный конструктор объектов, а также вызывает новый оператор для фактического выделения памяти... но, видимо, это обычно не видно.

Если у вас нет виртуальных базовых классов, [первые два] являются тождественны; GCC будет, на достаточных уровнях оптимизации, фактически псевдонимом символы для одного и того же кода для обоих.