Pull to refresh

Comments 34

Для полноты картины можно также поговорить о слабой связи, ключевое слово inline. Если применять этот вид связи можно также обходить One Definition Rule (например писать реализации функций в хедере), как и в случае со static. Но механизм тут другой.

Когда используешь static, символы помечаются как внутрение, и One Definition Rule не нарушается. При inline символы остаются внешними (видны всем), но когда линковщик видит несколько одинаковых слабых символов, он оставляет только один из них, а остальные отбрасывает (как именно не стандартизированно, но 99%, что оставит тот, что увидел первым).

Тут есть небольшой плюс в том, что в бинарник попадет только один экземпляр кода, а не N (размер бинарника меньше). Однако с этим нужно быть очень осторожным. Вы вполне можете случайно (ну или специально) сделать несколько реализаций inline функций с одинаковым именем. Тогда программа будет работать немного странно =) Вот тут я набросал такой пример, где вывод программы зависит от того, в каком порядке передавать линкеру object файлы.

Ну и напоследок — все шаблонные функции обладают слабым связыванием.
Добавьте еще в пример поведение со включенной оптимизацией…
ооо, прикольно, об этом я не знал. То есть тут ещё и разное поведение в дебаге и релизе… В пример добавлю.

Для остальных: при оптимизации компилятор может честно заинлайнить эти функции и вообще не добавить эту функцию в таблицу символов.

Он это может сделать, видимо, потому-что несколько inline функций с разными реализациями — это Undefined behavior (не проверял, это мое предположение), а компилятор считает, что UB не существует => функции все одинаковые => если получится заинлайнить, то можно вообще функцию выбросить.
Ну, на самом деле он это может сделать прежде всего потому что именно в этом и заключается смысл инлайна — в том, что функция вообще не должна попадать в бинарник.

А все остальное: слабые символы, UB при их различии — добавлено уже для того чтобы этот самый inline стал возможен.
А все остальное: слабые символы, UB при их различии — добавлено уже для того чтобы этот самый inline стал возможен.
Всё-таки это для обслуживания случая, когда inline невозможен добавлено было. Чтобы программа вдруг не превращалась из валидной в невалидную в зависимости от настроек оптимизатора…

Но так-то да, «штатный» режим — это когда inline-функции в файл не попадают…
Я бы сказал так, inline был для того, чтобы код функции напрямую подставлять в код вызывающей функции. А вот должна ли она при этом оставаться в бинарнике мне не очень очевидно (я вообще зарекся использовать это слово вместе со словом оптимизация =) ).
Посылаю голову пеплом… Пока я писал комментарий о том как очевидно, что случится при оптимизации… для кого-то это оказалось неожиданностью… Так бывает…
Там как раз всё просто. Но вообще инлайн-функции и переменные с разными определениями в разных единицах компиляции — это неопределённое поведение. Вплоть до того, что легко представить случай, когда это приведёт и к пресловутому «rm -rf» (хотя не уверен, что можно заставить такое существующие компиляторы проделать).

Кстати, статические локальные переменные всегда zero-initialized.

Увы, но не всегда.
Хоть стандарт и требует, чтобы они такими были, но разработчики компиляторов иногда имеют свое мнение. Например компилятор TI для архитектуры C2000 не инициализирует статические локальные переменные. В мануале они это решение обьясняют желанием ускорить старт прошивки. Об инициализации предлагают пользователям самим озаботиться.
Так это не компилятор должен делать, а среда выполнения перед запуском программы (для С в ISO/IEC 9899:TC3 5.1.2) Если у вас bare-metal, то это должно делаться start.S или его аналоге.
Заголовок спойлера
у нас, например, arm-none-eabi-gcc-6.3.1 кладет это в bss-секцию, которую руками очищаем при старте.
У вас неправильный код инициализации. Для GCC/ARM если вы код вот так в лоб запустите, то тоже не будет нулевых значений — нужен код для инициализации сегментов BSS и DATA. Обратите внимание, что у вас, скорее всего, ещё и начальные значение переменным не присваиваются после старта — явный признак того, что в стартап-коде чего-то не хватает.
Понятно, что это проблема в коде инициализации. Но дело в том, что все что исполняется до main() поставляется производителем контроллера. Мы, иногда даже не видим, что работает раньше, а если и видим то иногда менять не имеем права. Описанное поведение, оказалось не проблеммой, а намерянными действиями производителя тулчейна.
Собственно мой коментарий был только о том, что если у вас нет контроля над тем что исполняется до main() то не стоит надеяться, что нстатические локальные переменные будут всегда проинициализированными нулем. Более переносимое решение, делять явную инициализацию.
Предположим есть функция загрузки изображения LoadImage которая по расширению файла определяет какой формат изображения будет использоваться (например jpg,png,tiff,webp,...) и реализация лежит в статической библиотеке в куче объектных фалов. Как в таких случаях принято в современном C++ указывать линковщику какие форматы файлов я хочу чтобы прилинковались, а какие нет.
В С++ нет динамического связывания. Так что или через явное использование функций (тогда неиспользуемые не линкуются), или через макросы.
Можно через реестр и глобальные конструкторы, но с библиотеками это работать не будет, нужно оставлять (и линковать) объектные файлы.

Можно реализацию — в библиотеку, а в микрообъектых файлах в каждом только функцию-конструктор, которая зарегистриует формат и всё.
Мы про линковку. Предлагаемые вами приседания на неё не влияют. Ну разве что вы этих «микрообъектных» файлах прикрутите динамическую загрузку библиотек и создание прокси-объектов к ним, что не портабильно никак и является весьма низким уровнем системного программирования.
Вы сейчас серьёзно или издеваетесь? Вы хотите что вот так сделать не умеете:

$ g++ -c *.cc
$ g++ main.o registry.o gif.o jpg.o png.o -o all-formats
$ ./all-formats 
gif
jpg
png
$ g++ main.o registry.o gif.o -o gif-only
$ ./gif-only 
gif

Мне казалось, что так любой уважающий себя программист на C++ должен такое уметь. Proof-of-concept — под спойлером, грязноватый, правда, но суть передаёт. Ну элементарно же! Всё в рамках стандарта…
sources
formats.h
#ifndef FORMATS_H
#define FORMATS_H

extern void RegisterFormat(const char*);

class Registered {
 public:
  Registered(const char* format) {
    RegisterFormat(format);
  }
};

extern void PrintRegisteredFormats();

#endif

registry.cc
#include <iostream>

#include "formats.h"

namespace {

const char* formats[10];
int format_count = 0;

}

void RegisterFormat(const char* format) {
  formats[format_count++] = format;
}

void PrintRegisteredFormats() {
  for (int i = 0; i < format_count; ++i) {
    std::cout << formats[i] << std::endl;
  }
}

main.cc
#include "formats.h"

int main() {
  PrintRegisteredFormats();
}

gif.cc
#include "formats.h"

static Registered format("gif");

jpg.cc
#include "formats.h"

static Registered format("jpg");

png.cc
#include "formats.h"

static Registered format("png");

registry.o и всё реализацию — можно в библиотеку засунуть, но «ключевые» файлы gif.o/jpg.o/png.o — нельзя (это уже не стандарт C++, это уже устройство библиотек… но они, в общем, на всех платформах одинаково работают).
Да, я сразу как-то не понял, видимо.
Но оно компиляторо- и платформозависимо и вообще не по стандарту.
Например, линкер имеет полное право выбросить ваш static объект, как неиспользуемый, нет? На практике он этого, скорее всего, не сделает, из-за того, что это традиционные костыли, но всё же. Порядок инициализации реестров и самого реестра, тоже зависит от порядка линковки и фаз Луны.
И не дай Бог где-то что-то забыл, какие-то горизонтальные связи, и всё — у тебя тихо-мирно ликнуется куча неиспользуемого мусора, о котором ты даже и не подозреваешь.
Ещё можно через weak-references сделать. Тоже компиляторо-зависимо, но все поддерживают.
Но оно компиляторо- и платформозависимо и вообще не по стандарту.
Что значит «не по стандарту»? Если эти файлы, каким-то образом, окажутся частью вашей программу — то они таки отработают ровно как написано — в полном соответствии со стандартом.

А как работают библиотеки — это да, «не по стандарту»… ну так в стандарте библиотеки вообще не рассматриваются. Вообще. Никак. В принципе. Как определяется какие файлы в программу входят, какие нет — стандарт не описывает совсем.

Собственно именно поэтому так тяжело ввести модули в язык — он традиционно не указывал в этом месте ничего и у разных производителей, разумеется, разные идеи.

Например, линкер имеет полное право выбросить ваш static объект, как неиспользуемый, нет?
Нет. Если вы задали объектный файл в качестве аргумента — то вы, тем самым, чётко дали понять что уж как минимум содержимое этого объектного файла вы хотите в программе увидеть. Иначе непонятно что мешало бы линкеру и все другие файлы, которые компилятор ему подсовывает (всякие crtbeginS.o и crtendS.o) выкинуть.

На практике он этого, скорее всего, не сделает, из-за того, что это традиционные костыли, но всё же.
Оличная идея: взять функцию, ради которой программы была изначально создана — и объявить её «костылями».

«Возьми вот эти вот объектные файлы, сложи их вместе и свяжи их между собой» — это то, чем линкер занимался ещё до моего (и, скорее всего, вашего) рождения в OS/360 и RSX-11. Да-да, FORTRAN 66 и всё вот это вот. В те времена он даже назывался «link editor», потому что он вообще не решал никаких других задач: все объектники, которые он получал он «склеивал» друг за другом в большой модули и «связывал» вместе переменные из разных модулей. И всё.

Да, потом на него навесили разные дополнительные задачи, научили работать с библиотеками, выкидывать ненужный код (всякие --gc-sections)… но основную работу у него никто пока не отбирал… уж очень много на это поведение всего завязано.

И не дай Бог где-то что-то забыл, какие-то горизонтальные связи, и всё — у тебя тихо-мирно ликнуется куча неиспользуемого мусора, о котором ты даже и не подозреваешь.
Ну это уже другая история.

Ещё можно через weak-references сделать. Тоже компиляторо-зависимо, но все поддерживают.
А вот это — уже выходит за рамки стандарта. Хотя на практике, зачастую, удобнее, да.
Не по стандарту, это порядок инициализации статическикй переменных между модулями и поведение компилятора/линкера в случае неиспользуемых объектов.
поведение компилятора/линкера в случае неиспользуемых объектов
Это вне рамок стандарта просто — но почти все известные науке (и точно все популярные) действуют одинаково.

порядок инициализации статическикй переменных между модулями
А это как раз неважно — гарантируются что все они будут вызваны до начала работы основной программы, а как сделать так, чтобы от порядка вызова ничего не зависело — уже дело техники.
Уже пяток вариантов ответа удалил :)

Понял, что все они зависят от ответа на вопрос «Как и где реализована связь между расширением и форматом?»
UFO just landed and posted this here
Вопрос к уважаемому автору: почему вместо устоявшегося в литературе термина «связывание» Вы используется англицизм «линковка»? Слова такого в русском языке вроде не наблюдается, и звучит как «приспособа», чем-то из токарно-столярного дела отдает.
Да? А я думал что линковка это и есть устоявшийся термит.
Не уверен, что речь об одном и том же.

Препроцессор препроцессит (никогда так не говорю), компилятор компилирует, линковщик (или линкер?) линкует.

Линковка это низкоуровневый процесс, который дает нам то, что можно назвать ранним связыванием.

Линковку можно выполнить в рантайме, при загрузке динамической библиотеки и получить позднее связывание.

Позднее связывание можно получить и при использовании статической линковки, например, при использовании указателей на функции или виртуальных методов. Линковка уже прошла, но какая именно реализация под указателем будет понятно в рантайме (во время работы), на этом построены многие паттерны (шаблоны уже заняты).

Пусть уж лучше линковка останется линковкой. Уже были переводы applet как «приложеньице». У нас же нет цели стихи про березы и линковку писать :)

Разве есть такое слово "линковщик"? Компиляция и компилятор — это слова русского языка, а вот препроцессор с линковщиком — нет. Кстати, почему линковщиком, а не линкование? Или ещё хорошее слово: "препроцессировка", здорово, правда? Не знаю, есть английский глагол to link, по нашему связывать. Назначение программы linker состоит в связывании различных модулей компиляции в модуль исполнения, где тут "линковка" совершенно непонятно. Предлагаю ввести в обиход слова: мапировка, хэшировка, лукапирование и т.п.

Компиляция и компилятор — это слова русского языка, а вот препроцессор с линковщиком — нет.
И «препроцессор» и «линкер» — достаточно часто встречаются даже и в документации, а уж в статьях — так и подавно. Статья Препроцессор Си даже в энциклопупии есть. И линковка, кстати, тоже: Компоновщик (также редактор связей, от англ. link editor, linker) — инструментальная программа, которая производит компоновку («линковку»).

А на слово линкер аж два варианта «на выбор» предлагается: «синтетический олигонуклеотид, присоединяемый с помощью ДНК-лигазы к фрагменту ДНК для того, чтобы придать концам этого фрагмента структуру с заданными свойствами» и, собственно «компоновщик» (думаю быть ему линкером в конце-концов).

Предлагаю ввести в обиход слова: мапировка, хэшировка, лукапирование и т.п.
Будут достаточно часто нужны — появятся. Хватит уже этой борьбы за мокроступы.
Тут есть две темы:

— расширение русского языка новыми терминами — могу поучаствовать как тролль, но лучше пропущу, хотя это хорошая и серьезная тема

— разделение понятий связывания имен на стадии ld и связывание конкретных объектов во время исполнения. Так получилось, что первая стадия просто исторически часто называется линковкой, а вторая стадия (точнее даже не стадия, а трактовка поведения), более свежая и молодая уже именуется поздним/ранним связыванием. Проблема такая же как и в случае с шаблонами проектирования, которые стали путаться с шаблонами языка. Зато теперь у нас есть прекрасные русские слова «паттерн» и «темплейт» :)

Еще есть слабое связывание через weak атрибут

За рамками стандарта вообще много чего есть — но это уже отдельную статью писать нужно.
Sign up to leave a comment.