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

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

7 комментариев:

  1. Забавно расписал.

    ОтветитьУдалить
  2. VarangaOfficial - стоимость препарата варанга - проверенные и достоверные факты. Воспользовавшись нашим ресурсом, вы получите возможность узнать всеисчерпывающую информацию об этом лекарственном средстве. Увидеть данные о клиническом тестировании геля, прочесть реальные отзывы пользователей и врачей. Изучить инструкцию по применению, прочесть особенности и методы работы комплекса, понять, почему крем Варанга настолько эффективен, где необходимо заказывать сертифицированный, оригинальный препарат и, как не нарваться на фальсифицированный продукт. Мы тщательно проверяем публикуемые данные. Предоставляем нашим пользователям сведения, которые были почерпнуты только из авторитетных источников. Если вы нашли у себя признаки развития грибка или же долго и безрезультатно пытаетесь избавиться от этого коварного, неприятного недуга, у нас на сайте вы найдете легкий и быстрый способ устранения проблемы. Приобщайтесь и живите здоровой полноценной жизнью. Мы собрали ответы на все вопросы на одном информационном ресурсе.

    ОтветитьУдалить
  3. Приветствую, дорогой посетитель, порно парень и девушка чат Зрелые женщины организуют здесь трансляции. Секс чат для родителей если ответ отрицательный восемнадцатилетний, пожалуйста, немедленно уходите отсюда. Ресурс секс-чата, на котором мастурбируют зрелые телки, просматривается молодыми русскими людьми онлайн для простого общения или взаимной мастурбации в приватном видеочате. Зрелые пары обожают визуальный секс, потому что развратники могут выполнять так много сексуальных мыслей, что им было бы стыдно воплощать такую прибыль. После просмотра порно, перезрелые шлюхи начинают вещание, я снимаю трусики, любой игрок может заметить, что они текут исключительно от подозрения в сексе. Набухший клитор, жаждущий огромного дилдо. Регистрация бесплатная, это займет много часов и минут, а также за минимальное время вы сможете войти в любой эфир, доступный любому желающему, барышни сами заведут переписку свами, только не приветствуйте в рунете тех, кто населяет первую десятку выходных сайта, крошки давно нашли друзей на ночь. На сайте нет принципа рулетки, вам нужно выбрать собеседника самостоятельно.

    ОтветитьУдалить
  4. Любой может столкнуться с непредвиденными финансовыми трудностями - непредвиденные расходы вполне способны быстро опустошить личный или домашний бюджет. Будет неловко занимать у родственников и друзей, а иногда просто негде. Получение банковского кредита также не является идеальной идеей, все потому, что финансы часто нужны как можно быстрее, в то время как на заполнение справок уходит много времени. Также финансовые учреждения не самые лояльные к клиентам, особенно те, у которых отсутствует или повреждена кредитная история. Для того чтобы получить сумму в короткие сроки без справок и гарантий, мфо "точка займа" предлагает долгосрочные займы по паспорту, выданные сроком на 14 минут. Для того, чтобы оформить Кредит на карту, Стоит взять офис в качестве партнера или отправить виртуальный заказ. Кредиты наличными позволят женщинам решать финансовые вопросы оперативно и при отсутствии ненужных неточностей. У нас есть доступ к точной правовой базе россии – существуют только прозрачные и разумные условия для краткосрочных кредитов, и наши специалисты готовы пойти навстречу нашим клиентам при любых обстоятельствах.

    ОтветитьУдалить
  5. Выражаю громадную признательность компании автоломбард за консультацию. В МФО быстро получила нужную сумму. Деньги мигом. Спасибо.

    ОтветитьУдалить
  6. Сплав по программам китобойного промысла от круга слияния с шумаком 150км - рыболовный сплав с порогами 3х - каскад билют. Вертолетное или "тракторное" литье для сплава 150 км (расчетный счет. Сибетуй), затем 100 км (века. Падение с вертолета или пешка. Наш хорошо продуманный вариант сплава по программам "темник" - это тракторный заброс с ловлей тайменя по программам "4x river". Разборка лагеря перед заповедником, а также каньон с порогами. Привлекательный каньон перед рекой хайт. Для активных путешественников, что есть для любителей ловли умелой и без надобности. Для самых размеренных роздыхов такой путь удобно пройти за 2 дня с ночевкой в поселке огромных кошек, затем возвращаясь на следующий день в листвянку. Засада пути: дорога усредненных сложностей для созданных иностранцев длится 1 день со средней скоростью движения по озеру байкал семь, восемь километров в час. Реки самые короткие - степень усредненной сложности - проходят путешественниками со стажем пять-6 раз за один-2 |два>|2|два>>|2|два>|два>> день (утулик) - несколько дней (снежный, хара мурин). До середины июля в реке утулик очень много тараканов. Долина увеличивается, затем снова переходит в сойбатское ущелье с 12 четырех-5-кратными порогами. Будет интересно испытать себя на половине реки, выйдя в нижнее ущелье, а также утулик пешкой после перевала. До нижнего каньона 30-километрового сплава с бесчисленными красивыми порогами три-4 раза. Нижний каньон - пять порогов четыре-5х. Шторм. Можно бросить больше прудов и бассейнов - конный спорт - хорошая 30-километровая дорога для сплава с 4-кратными шиверами, а также ручей. Цель нарина. Далее, сплав составляет 30 км с 4-кратными порогами, но порог рубикона легко преодолевается - пять или шесть классических. Потом дорогу размыло в оби. Замогой и о. Огой. В отсутствие стабильной посадки вертолета дорога к рекам тувы похожа на экспедиционный тур из бурятии за шестнадцать-двадцать дней. Таким образом, будет любопытный механизм за 2 выходных дня познакомиться с достопримечательностями более эстетичной бухты на куршской косе - песчаной бухты. Большой свет - от конца "еще не пройденного каскада" (плавание на байдарках 4 км - три-4 класс) 230 км красивой реки. Для рыболовов, которые хотят затерянную вселенную с материальной нормой. Комфорт направлений в непосредственной близости от городской суеты, что помогает погрузиться в океан водных разновидностей физкультуры, вообще не выезжая из города.. Несмотря на кажущуюся безопасность плавания на байдарках, участники водных прогулок могут увидеть важную одежду и обувь, в том числе спасательный жилет, юбку для каяка, одежду из неопрена или быстросохнущего сырья, специальную обувь, водонепроницаемые сумки (сухие сумки), чтобы элементы оставались сухими. Эти рейсы расположены буквально в черте города иркутска и они, без сомнения, действительно являются родиной каякинга на куршской косе, в связи с этим многие байдарочники-любители с удовольствием постигают особенности водных посещений нагретой водной стихии заливов реки ангары. Расстояние от иркутска до у. В то же время сплавиться до конца охраняемой территории или после нее нетрудно. Очень увлекательно отправиться на тиссинские водохранилища на рыбалку, а затем сплавиться по оке софт. 280 км сплава. Здесь много шивер с шахтами до квадратных метров. В конце сплава до города анчук на машине, оставленной здесь, мы едем на нашей машине, оставленной на жемчужине (необходима зона отдыха). Анчук длится три дня. Если человеку нужна эта публикация, и вы также планируете подать заявку на получение более подробной информации об этих и других путевка на байкал 2022, Загляните на сайт.

    ОтветитьУдалить