@dendibakh

Как трактовать определение в двух разных модулях классов с одинаковым именем?

Всем добрый день!
Подскажите, пожалуйста, как трактовать с точки зрения стандарта С++ код, представленный ниже? Он компилируется и компонуется без ошибок и предупреждений, однако работает непредсказуемо. Проверял на gcc 4.8.1 и msvc 2013.
$ cat a.cpp
#include <stdio.h>

struct A
{
   void foo() { printf("a.cpp: A::foo()\n");}
};

void fooA()
{
   A a;
   a.foo();
}

$ cat b.cpp
#include <stdio.h>

struct A
{
   void foo() { printf("b.cpp: A::foo()\n");}
};

void fooB()
{
   A a;
   a.foo();
}

$ cat main.cpp 
void fooA();
void fooB();

int main()
{
      fooA();
      fooB();
      return 0;
}

$ g++ main.cpp a.cpp b.cpp 

$ ./a.out
a.cpp: A::foo()
a.cpp: A::foo()

Проблема в том, что в большом проекте Вы можете даже и не заметить, что объявили класс с уже существующим именем. Например, в тестах на скорую руку написали вспомогательный класс и поехали дальше. Потом расхлебываете, как было в моем случае. :) Компилятор и компоновщик при этом молчат.
Понятно, что так лучше не писать, но все же. Недавно на хабре рассуждали о разыменовании нулевого указателя. Так давайте же дадим оценку и такому коду.
  • Вопрос задан
  • 2453 просмотра
Решения вопроса 2
EvgenijDv
@EvgenijDv
C/C++ programmer
В общем немного покопавшись нарисовалась вполне ожидаемая ситуация:
В получившемся исполняемом файле вызывается одна и та же версия метода A::foo()
objdump -d a.exe
  00401600 <__Z4fooBv>:
  call   402890 <__ZN1A3fooEv>
  
0040161c <__Z4fooAv>:
  call   402890 <__ZN1A3fooEv>
 
00401638 <_main>:
  401643:       e8 d4 ff ff ff          call   40161c <__Z4fooAv>
  401648:       e8 b3 ff ff ff          call   401600 <__Z4fooBv>

После компиляции в объектных файлах используется одно и тоже имя для метода A::foo()
objdump -t b.o

b.o:     file format pe-i386

SYMBOL TABLE:
[  0](sec -2)(fl 0x00)(ty   0)(scl 103) (nx 1) 0x00000000 b.cpp
File
[  2](sec  5)(fl 0x00)(ty   0)(scl   3) (nx 1) 0x00000000 .text$_ZN1A3fooEv
AUX scnlen 0x17 nreloc 2 nlnno 0 checksum 0x0 assoc 0 comdat 2
[  4](sec  5)(fl 0x00)(ty  20)(scl   2) (nx 1) 0x00000000 __ZN1A3fooEv
AUX tagndx 0 ttlsiz 0x0 lnnos 0 next 0
[  6](sec  1)(fl 0x00)(ty  20)(scl   2) (nx 0) 0x00000000 __Z4fooBv

objdump -t a.o

a.o:     file format pe-i386

SYMBOL TABLE:
[  0](sec -2)(fl 0x00)(ty   0)(scl 103) (nx 1) 0x00000000 a.cpp
File
[  2](sec  5)(fl 0x00)(ty   0)(scl   3) (nx 1) 0x00000000 .text$_ZN1A3fooEv
AUX scnlen 0x17 nreloc 2 nlnno 0 checksum 0x0 assoc 0 comdat 2
[  4](sec  5)(fl 0x00)(ty  20)(scl   2) (nx 1) 0x00000000 __ZN1A3fooEv
AUX tagndx 0 ttlsiz 0x0 lnnos 0 next 0
[  6](sec  1)(fl 0x00)(ty  20)(scl   2) (nx 0) 0x00000000 __Z4fooAv

Ну и судя по всему, во время линковки, линковщик находит первую реализацию _ZN1A3fooEv и дважды подставляет ее, ведь и в одном и другом объектном файле используется одно и тоже имя. Непонятно только почему он не стал искать вторую реализацию этой функции во втором объектном файле... Может стоит переместить этот вопрос на SO? Я думаю там могут дать более развернутый ответ.
Ответ написан
@MarkusD Куратор тега C++
все время мелю чепуху :)
Именно такую ситуацию имел в команде пару месяцев назад. Человек использовал небольшой вспомогательный класс внутри реализации (.cpp) юнит-тестов. И оба раза назвал этот впосогательный класс одинаково. Код в этом случае прекрасно компилируется, только работает как граната.

С точки зрения стандарта ситуация трактуется как штатная. Компиляция производится независимо для каждого файла, объявления + реалиции этих самых 'struct A' укладываются по своим объектным файлам, а потом ликовщик увязывает этот код как получится. В результате создаваемые экземпляры не всегда могут соответствовать локальному описанию.

Обычный линковщик ожидает что на вход к нему будут поданы уже готовые к линейной линковке блоки. Обычный линковщик, если его не попросить, связывает только уже используемые участки кода, начиная с точки входа или точек экспорта. Сортировка блоков (библиотек и модулей) обычно топологическая, но с сохранением алфавитного порядка между модулями в рамках одного ранга. Вот и получается, что "ликовщик увязывает этот код как получится".
Линковщик в очередном модуле встречает еще не связанное, но уже используемое где-то имя типа и генерирует для этого типа код. Далее, в другом модуле линковщик снова встречает имя этого же уже связанного типа и просто отбрасывает его. Но отбрасывается не весь тип, а только уже связанные его части. Если во втором рассматриваемом типе будет находиться иной набор функций, они будут подвязаны к набору функций уже встроенного типа. И вот с этого места начинается дорога в ад, т.к. у двух таких типов может быть разный размер, разные поля в состоянии, разное выравнивание, разная реализация одинаково названных функций.

Выход из ситуации:
  • использовать Forward declaration для таких локальных классов (добиться ошибки 'class redefinition' в таких случаях);
  • обертывать описания в локальные неименованные namespace (добиться уникальности пространства для таких классов);
  • описывать такие локальные классы как nested-классы от глобальных (делать хотяб Forward declaration в пространстве глобального класса - тоже упор на уникальность пространства);
Ответ написан
Пригласить эксперта
Ответы на вопрос 2
@jackroll
Сверхразум
Пользуюсь MSVS 2013. Вылезает ошибка C2011.
Используйте namespaceы или #ifdef-#ifndefы
Ответ написан
Комментировать
AxisPod
@AxisPod
Опции линкера надо ковырять, может что и есть в этом плане.
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы