Прикладные linkage types в C++ 🔗
Стандартное добавление функции в C++ происходит так: в
.h-файле пишется объявление функции (которое попадает в много translation unit), а в одном .cpp-файле пишется определение функции.Чтобы определить функцию в
.h-файле, чаще всего пишут спецификатор inline (это рекомендуемый путь), в несколько раз реже static.Посмотрим, каким будет LLVM IR (компиляторное промежуточное представление C++-кода) для
int sum1(), static int sum2(), inline int sum3(): ссылка на godbolt.У LLVM IR есть интересный список linkage types для символов, типов много. Linkage type определяет, как ведет себя символ во время линковки в бинарник. (Символ - это функция или глобальная переменная)
▪️ У
sum1 дефолтный linkage type external - возможно ровно 1 определение символа. Линковщик выдаст ошибку, если в программе будет 0 или >1 определения.▪️ У
sum2 linkage type internal - символ доступен только из того translation unit, где определен. Если в процессе линковки попадется одноименный символ, то линковщик просто переименует sum2, чтобы не было коллизии.▪️ У
sum3 linkage type linkonce_odr. Это типичный weak symbol, как и некоторые другие типы (linkonce, weak).В программе может быть несколько weak-определений одного и того же символа.
Если все определения символа являются weak, то линковщик берет рандомное определение.
Если какое-то из определений символа является strong (как у
sum1), то линковщик берет strong-определение.В чем отличие
linkonce от linkonce_odr?▪️ В общем случае weak-определения могут быть разными, поэтому компилятор не имеет права заинлайнить вызов weak-функции (после линковки у функции может оказаться совсем другое определение)
▪️ Но Стандарт С++ требует от программиста обеспечить, чтобы inline-функции имели идентичное определение (это естественным образом достигается, если определение находится в
.h-файле)▪️ Поэтому компилятор имеет право заинлайнить вызов метода
sum3 - программа от этого сломаться не сможетСкомпилируем объектный файл
> clang++ -c link.cppНа Linux получим объектный файл формата ELF. Используем утилиту
readelf для чтения таблицы символов. Чтобы вывести исходные имена методов (не mangled-имена), используем утилиту c++filt:> readelf -s link.o | c++filtПолучим такой вывод (опустил другие символы):
Symbol table '.symtab' contains 21 entries:В общем случае выгоднее использовать inline-методы, чем static-методы! Потому что так меньше загрязняется бинарник, и все статические переменные внутри функции живут в количестве 1 штуки, а не в N штук:
Num: Value Size Type Bind Vis Ndx Name
6: 00000000000000c0 18 FUNC LOCAL DEFAULT 2 sum2(int, int)
14: 0000000000000000 18 FUNC GLOBAL DEFAULT 2 sum1(int, int)
20: 0000000000000000 18 FUNC WEAK DEFAULT 7 sum3(int, int)
inline int* get_address() {
// `dummy` займет sizeof(int) памяти, а со static-методом было бы N*sizeof(int)
static int dummy;
// inline-метод - возвращает один и тот же указатель
// static-метод - уникальный для каждого translation unit
return &dummy;
}