Если спецификация языка говорит, что ошибки быть не должно, значит ошибки быть не должно. А дальше уже начинаются детали реализации. Почему вы решили, что они не дублируются?
В классической реализации инлайновые функции с внешним связыванием для которых компилятор при компиляции нескольких единиц трансляции решил сгенерировать тела, разумеется, дублируются. В процессе компиляции каждая единица трансляции получает свою копию такой функции в своем объектном файле с одним и тем же именем.
Однако такие функции в объектном файле помечены особым образом - так, чтобы при обнаружении множества копий одного и тот же внешнего символа при линковке линкер не выдавал ошибки, а наоборот молча удалял все копии, оставляя только одну. То есть компилятор 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) такой подход не прижился.