Сложности линковки

В очередной раз столкнулся с довольно забавным случаем из исходников. Что характерно это SPEC, и в нём обнаружилась ошибка (уже вторая с которой я столкнулся!). Причём для проявления ошибки должны были очень удачно сложиться звёзды. Я не буду показывать весь SPEC, а рассмотрю только маленький примерчик.

//------ t1.cpp

#include <stdlib.h>
#include <stdio.h>

//namespace
//{
class A
{
public:
    A(){printf("1\n");a=1;}
    int a;
};
//}

void foo(void * a)
{
    a = new A;
}

//------ t1.h

void foo(void * a);

//------ t2.cpp

#include <stdlib.h>
#include <stdio.h>

//namespace
//{
class A
{
public:
    A(){printf("2\n");b=1;}
    int b;
};
//}

void bar(void * a)
{
    a = new A;
}

//------ t2.h

void bar(void * a);

//------ main.cpp

#include "t1.h"
#include "t2.h"

int main()
{
    void * a;
    foo(a);
    bar(a);
}

Из исходника видно, что при работе `foo' вызывается конструктор объекта `A' из файла `t1.cpp', который выводит `1', а при работе `bar' вызывается конструктор объекта `A' из файла `t2.cpp', который выводит `2'. Смотрим что у нас получается по факту:

$ g++ *cpp
$ ./a.out
1
1

Если посмотреть дизассемблер, то чётко видно, что вызываются одинаковые функции:

080485dc <_Z3fooPv>:
...
 80485f5:    e8 1e 00 00 00           call   8048618 <_ZN1AC1Ev>
...

08048638 <_Z3barPv>:
...
 8048651:    e8 c2 ff ff ff           call   8048618 <_ZN1AC1Ev>

Теперь о том почему так получается. В стандарте есть понятие `linkage':

3.5 Program and linkage 2. A name is said to have linkage when it might denote the same object, reference, function, type, template, namespace or value as a name introduced by a declaration in another scope: - When a name has external linkage, the entity it denotes can be referred to by names from scopes of other translation units or from other scopes of the same translation unit.

Здесь говорится, что имя имеет «linkage» когда оно указывает на тот же объект по имени в другом scope'е. При этом если «linkage» идёт как external, то связывание производится из разных translation units, коими являются наши cpp файлы.

Здесь стоит заметить, что хотя в разных модулях у нас разные объекты, имя у них идёт одинаковое, а связывание производится именно по имени.

Далее смотрим

1.4 Implementation compliance 6. The templates, classes, functions, and objects in the library have external linkage (3.5)

Т.е. здесь чётко говорится, что наши классы имеют external linkage.

Для обхода этой ошибки рекомендуется поместить оба класса в безымянные пространства имён (они закомментированы в примере). Тогда оба имени `A' будут находится в разных scope'ах, и не будут пересекаться:

080485f9 <_Z3fooPv>:
...
 8048612:    e8 c5 ff ff ff           call   80485dc <_ZN12_GLOBAL__N_11AC1Ev>
...
08048655 <_Z3barPv>:
...
 804866e:    e8 c5 ff ff ff           call   8048638 <_ZN12_GLOBAL__N_11AC1Ev>
...

Теперь то, что не вошло в первую редакцию поста.

Попробуем скомпилировать с оптимизацией:

$ g++ *cpp -O1
$ ./a.out
1
2

На самом деле это чистое совпадение, связанное с тем, что с -O1 включается inline, и конструкторы тупо подставляются в тела вызывающих функций.

Более того такое поведение компилятора совершенно законно. В стандарте есть «3.2 One definition rule», который для таких случаев гласит следующее:

5. There can be more than one definition of a class type (Clause 9), enumeration type (7.2), inline function with external linkage (7.1.2), class template (Clause 14), non-static function template (14.5.6), static data member of a class template (14.5.1.3), member function of a class template (14.5.1.1), or template specialization for which some template parameters are not specified (14.7, 14.5.5) in a program provided that each definition appears in a different translation unit, and provided the definitions satisfy the following requirements. Given such an entity named D defined in more than one translation unit, then - each definition of D shall consist of the same sequence of tokens; and ... Тут ещё несколько сложных правил ... ... If the definitions of D satisfy all these requirements, then the program shall behave as if there were a single definition of D. If the definitions of D do not satisfy these requirements, then the behavior is undefined.

Т.е. здесь говорится, что в разных translation unit может быть несколько определений одного и того же класса при определённых условиях. Меня эта формулировка очень удивила, но в принципе она довольно логична. В приведённом примере нарушаются условия, т.о. пример имеет UB и компилятор может делать с ним вообще всё что угодно. Т.е. разное поведение на -O0 и -O1 в данном случае совершенно допустимо.

Кстати, с 5-ой версии gcc научится такие ошибки отлавливать в режиме -flto:

$ g++ *cpp -Wall -flto && ./a.out
t1.cpp:4:7: warning: type ‘struct A’ violates one definition rule [-Wodr]
 class A
       ^
t2.cpp:6:7: note: a different type is defined in another translation unit
 class A
       ^
t1.cpp:8:9: note: the first difference of corresponding definitions is field ‘a’
     int a;
         ^
t2.cpp:10:9: note: a field with different name is defined in another translation unit
     int b;
         ^

Почему я так подробно написал об этой ошибке? Потому что столкнулся с ней я на примере, строка компиляции которого занимает 2 экрана, время компиляции пол часа, проявилось оно только в режиме компиляции всей программы, а свалилось оно на моей оптимизационной фазе из-за того что некорректно отработала предыдущая фаза, из-за того что к ней пришло некорректное представление. Далее пару вечеров обсуждали что именно произошло, кто виноват и что делать. В общем вроде бы мелочь, а столько веселья!