среда, 6 июля 2011 г.

Линковка в C++: проблемы множественного определения (multiple definition)

Регулярно пишу на C++ - хочется порой писать и о C++. 
Давно я вообще ничего сюда не писал - самое время совместить приятное с полезным. И с C++:)

Среди всех проблем, которые могут возникнуть на пути запуска C++-программы, есть такие, которые понять сложнее всего, - ошибки линковки. Одну из таких я попытаюсь сейчас описать: это ошибка множественного определения символов (multiple definition).

Вот, скажем, есть такой (искусственнейший) пример:

// main.cpp
#include "ComplicatedHello.h"

int main()
{
    ComplicatedHello obj;
    obj.SayHello();
    obj.MakeHoldeeSay();
}
Есть объект, который может сказать Hello сам, а может попросить это сделать другой объект, который содержит в себе.

// ComplicatedHello.h
#pragma once

#include "SimpleHello.h"

class ComplicatedHello
{
public:
    void SayHello()
    {
        Hello();
    }

    void MakeHoldeeSay()
    {
        m_Holdee.SayHello();
    }
private:
    SimpleHello m_Holdee;
};

// SimpleHello.h
#pragma once

#include "Hello.h"

class SimpleHello
{
public:
    void SayHello()
    {
        Hello();
    }
};

И простой объект, и сложный используют для общения одну единственную функцию Hello() - в ней-то и будет вся соль.

// Hello.h
#pragma once

#include

void Hello()
{
    std::cout << "Hello\n";
}

И всё прекрасно. Прелесть этой версии в том, что тут только один .cpp файл - одна единица компиляции, с которой все остальные файлы связаны цепочкой включений. Включение (#include) являет собой простое копирование всего кода подключаемого заголовочного файла, и поэтому в конце получается этакий разросшийся main.cpp. Следовательно, при компиляции его (заголовки не компилируются в объектники - только разве что прекомпилируются, если сильно надо) создаётся только один объектный файл - main.o - какие уж тут проблемы линковки:)

А теперь сделаем код более бизнесовым - разделим ComplicatedHello как самый громоздкий файл в нашем проекте на интерфейс и реализацию. Получаем такое:

// ComplicatedHello.h
#pragma once

#include "SimpleHello.h"

class ComplicatedHello
{
public:
    void SayHello();
    void MakeHoldeeSay();


private:
    SimpleHello m_Holdee;
};

// ComplicatedHello.cpp
#include "ComplicatedHello.h"
#include "Hello.h"

void ComplicatedHello::SayHello()
{
    Hello();
}

void ComplicatedHello::MakeHoldeeSay()
{
    m_Holdee.SayHello();
} 
Только при сборке на этот раз появилась неприятность: линковщик выдал ошибку multiple definition of 'Hello()'. Он мог бы ещё добавить, что это нарушение одного из фундаментальных правил C++ - правила одного определения (http://en.wikipedia.org/wiki/One_Definition_Rule), но сдержался. Так ведь самое главное, что всё вроде как предусмотрено, чтобы это правило не нарушать: каждый заголовок защищён прагмой, никаких циклических зависимостей, даже code style выдержан - ну что тут может не нравиться...

Но если вернуться к единицам компиляции, то станет видно, что их теперь две: main и ComplicatedHello. Можно проследить, что окажется включено в каждую из них:

main <- ComplicatedHello.h <- SimpleHello.h <- Hello.h
ComplicatedHello <- SimpleHello.h <- Hello.h

Правило одного определения нарушается, потому что функция Hello() вместе со своим телом полностью лежит в заголовке и переходит в изначальном виде в оба объектника. По стандарту языка в каждой единице трансляции допускается (и, более того, необходимо) определение одной и той же функции, если она объявлена как inline (и ещё какое-то исключение для статических функций), а для обычных функций на всю программу должно быть только одно определение где-то в одном месте.
Действительно, если определить Hello() как inline, ошибка исчезает, но зачем менять сигнатуру из-за такой мелочи, когда можно всё сделать как следует? То есть сделать из Hello.h самостоятельную единицу компиляции (может, так, наоборот, и не следует в этом случае - просто так хочется):

// Hello.h (да, вот такой целый хэдер)
#pragma once

void Hello();

// Hello.cpp
#include "Hello.h"

#include

void Hello()
{
    std::cout << "Hello\n";
}
Тогда каждое включение заголовка приведёт к копированию прототипа функции Hello() (типа как написать extern void Hello() вместо каждого включения), что создаст ссылку на эту функцию в каждом объектнике, а уж линковщик самостоятельно найдёт для каждой ссылки тело функции в её собственном объектнике.

Кстати, заметили, что определение класса SimpleHello оказывается тоже в каждом объектнике по схеме выше? Можно не беспокоиться, для классов по стандарту это ок:)

Таким образом, хотел просто предупредить, что нельзя сильно увлекаться с наполнением заголовочных файлов: они должны содержать прототипы функций, ну или тела inline-функций - не более, потому что любое лишнее определение может повлечь за собой зловещие и очень малопонятные ошибки, какими изобилует линковщик.

Кстати, хорошая новость: я и сам почти поверил, что разобрался в этом всём. Успеееех=)