Страницы

Поиск по вопросам

понедельник, 2 декабря 2019 г.

Почему inline-функции, определённые в заголовочных файлах не дублируются при линковке?

#cpp #функции #inline #линковка


Я прочёл такой факт насчёт "обычных" и inline- функций:


  В предыдущих главах мы не раз говорили, что вы не должны определять функции в заголовочных
файлах, так как если вы подключаете один заголовок с определением функции в несколько
файлов .cpp, то определение функции также будет скопировано несколько раз. Затем, при
соединении файлов, линкер выдаст ошибку, что вы определяете одну и ту же функцию больше
одного раза.
  
  Однако, встроенные функции освобождаются от этого правила, так как дублирования
в исходном коде не происходит — определение функции одно, и никакого конфликта при
соединении линкером файлов .cpp возникнуть не должно.


Почему нельзя определять функции в header'е, понятно: произойдёт "multiple definition"
при линковке нескольких модулей, включающих этот хэдер. Но из текста совсем не понятно,
почему это правило не работает для inline-функций. Я понял это так: "inline-функции
не дублируются, потому что они не дублируются". Бред.

Я, конечно, могу принять во внимание этот факт как должное, и всё, но всё таки хочется
понять, почему так происходит, а исчерпывающего объяснения этот текст не даёт.
    


Ответы

Ответ 1



Если inline-функция реально была заинлайнена, то её как бы и нету, поэтому она не может дублироваться в принципе. Если же она не была заинлайнена после оптимизаций, то она получает уникальное имя в каждой единице трансляции, в итоге мы имеем много функций, которые делают одно и то же, но имеют разные имена. Так как имена в итоге разные, multiple definition не происходит. Подробнее читать здесь: Стандарт C++11, 3.2.5 (One definition rule), 7.1.2 (Function specifiers). Где взять стандарт C++?

Ответ 2



Если спецификация языка говорит, что ошибки быть не должно, значит ошибки быть не должно. А дальше уже начинаются детали реализации. Почему вы решили, что они не дублируются? В классической реализации инлайновые функции с внешним связыванием для которых компилятор при компиляции нескольких единиц трансляции решил сгенерировать тела, разумеется, дублируются. В процессе компиляции каждая единица трансляции получает свою копию такой функции в своем объектном файле с одним и тем же именем. Однако такие функции в объектном файле помечены особым образом - так, чтобы при обнаружении множества копий одного и тот же внешнего символа при линковке линкер не выдавал ошибки, а наоборот молча удалял все копии, оставляя только одну. То есть компилятор C++ генерирует "свалку" одинаково именованных функций, раскиданных по разным объектным файлам, а линкер потом собирает всё вместе и занимается чисткой этой "свалки". В компиляторах семейства *nix эта пометка - это обозначение экспортируемого символа, как т.наз. "слабого" (weak) символа. В компиляторе MSVC++ существует аналогичная пометка selectany. Линкеры выдают ошибку множественного определения только если встретят два или более одинаковых "сильных" символа в процессе линковки. Если же "сильный" символ только один (а остальные "слабые"), то побеждает "сильный" символ, а "слабые" символы отбрасываются. Если "сильного" символа нет вообще, а есть только "слабые", то побеждает один (какой-то) из "слабых". Никакой ошибки при этом не рапортуется. Когда компилятор решает сгенерировать тело для inline-функции с внешним связыванием, он просто помечает соответствующий символ для линкера как "слабый" - и все. (На этом же механизме построена трансляция шаблонных функций, которые, как известно, тоже определяются в заголовочных файлах и тоже порождают свои копии во всех объектных файлах, которые потребовали их инстанцирования.) Например, скомпилировав вот такой простой исходник в объектный файл inline void bar() {} void (*foo())() { return bar; } и просмотрев содержимое этого объектного файла при помощи nm мы увидим 0000000000000000 W _Z3barv 0000000000000000 T _Z3foov Буковка W помечает "слабый" символ, а буковка T - "сильный" символ. Во всех объектных файлах, в которых сгенерировалось тело для такой inline функции, она будет фигурировать под одним и тем же именем _Z3barv с пометкой W. Обратите внимание, что ни о каком решении этой проблемы через генерацию множества функций с разными именами не может быть и речи: в всех остальных отношениях инлайновая функция с внешним связыванием должна вести себя так же как и любая другая функция с внешним связыванием, т.е., например, она обязана иметь один и тот же адрес во всех единицах трансляции. Побочным эффектом такого подхода является то, что "классический" подход к формированию объектного файла, в котором у функции нет начала и конца, а есть только точка входа, становится неприемлем. Для того, чтобы иметь возможность исключать функции из объектного файла, С++ компиляторы вынуждены формировать тела функций в объектном файле в компактном виде. Существуют исторические примеры альтернативных реализаций, которые пытались действовать по-другому. "Многопроходные" реализации вообще не порождали тел для инлайновых и шаблонных функций на первом проходе компиляции. Они выполняли предварительную линковку, на которой собирали информацию о том, каким функциям действительно нужны тела, затем снова вызывали компилятор и компилировали уникальные тела для таких функций, и затем уже выполняли финальную линковку. Но среди популярных компиляторов (GCC/Clang/MSVC) такой подход не прижился.

Комментариев нет:

Отправить комментарий