Visual studio статическая линковка

Задался вопросом как же правильно строить библиотеки для статической линковки. Вопрос возник после просмотра содержимого результирующего исполняемого файла. Дело в том, что после линковки некоторых библиотек в исполняемый файл попадает много незадействованного кода. Эксперимент оформил шелл-скриптом для *nix:

#!/bin/sh if ; then echo «Под учеткой рута работать отказываюсь.» exit fi # если clang не установлен — нужно заменить на gcc и g++ соответственно CC=»clang -Wall -pedantic -O3 -c» CXX=»clang++ -Wall -pedantic -O3 -c» LINK1=»clang -static -O3″ LINK2=»clang++ -static -O3″ # генерация библиотеки 1 ================================== # # все функции включаются в одну единицу трансляции # # ========================================================= printf «#include <stdio.h>\n\n» > testo-1.c for i in $(seq 20) do printf «void SomeFunc_$i() {\n» >> testo-1.c printf » printf(\»SomeFunc_$i\\\n\»);\n» >> testo-1.c printf «}\n\n» >> testo-1.c done $CC testo-1.c $CC -ffunction-sections -fdata-sections -o testo-1f.o testo-1.c ar rcs libtesto-1.a testo-1.o ar rcs libtesto-1f.a testo-1f.o rm -f *.c *.o # генерация библиотеки 2 ================================== # # каждая функция включается в отдельную единицу трансляции # # ========================================================= for i in $(seq 20) do printf «#include <stdio.h>\n\n» > testo-2-$i.c printf «void SomeFunc_$i() {\n» >> testo-2-$i.c printf » printf(\»SomeFunc_$i\\\n\»);\n» >> testo-2-$i.c printf «}\n\n» >> testo-2-$i.c done $CC testo-2-*.c ar rcs libtesto-2.a `ls testo-2-*.o` rm -f *.c *.o # генерация библиотеки 3 ================================== # # создается класс, где реализация методов включается в одну # единицу трансляции # # ========================================================= printf «#pragma once\n\n» > testo-3.h printf «class GodClass {\n» >> testo-3.h printf » public:\n» >> testo-3.h for i in $(seq 20) do printf » void SomeMethod_$i();\n» >> testo-3.h done printf «};\n» >> testo-3.h printf «#include <iostream>\n» > testo-3.cpp printf «#include \»testo-3.h\»\n\n» >> testo-3.cpp for i in $(seq 20) do printf «void GodClass::SomeMethod_$i() {\n» >> testo-3.cpp printf » std::cout << \»SomeMethod_$i\» << std::endl;\n» >> testo-3.cpp printf «}\n\n» >> testo-3.cpp done $CXX testo-3.cpp $CXX -ffunction-sections -fdata-sections -o testo-3f.o testo-3.cpp ar rcs libtesto-3.a testo-3.o ar rcs libtesto-3f.a testo-3f.o rm -f *.cpp *.o # генерация библиотеки 4 ================================== # # создается класс, где реализация методов разносится по # разным единицам трансляции # # ========================================================= printf «#pragma once\n\n» > testo-4.h printf «class GodClass {\n» >> testo-4.h printf » public:\n» >> testo-4.h for i in $(seq 20) do printf » void SomeMethod_$i();\n» >> testo-4.h done printf «};\n» >> testo-4.h for i in $(seq 20) do printf «#include <iostream>\n» > testo-4-$i.cpp printf «#include \»testo-4.h\»\n\n» >> testo-4-$i.cpp printf «void GodClass::SomeMethod_$i() {\n» >> testo-4-$i.cpp printf » std::cout << \»SomeMethod_$i\» << std::endl;\n» >> testo-4-$i.cpp printf «}\n\n» >> testo-4-$i.cpp done $CXX testo-4-*.cpp ar rcs libtesto-4.a `ls testo-4-*.o` rm -f *.cpp *.o # линкуем исполняемые файлы =============================== printf «void SomeFunc_1();\n\n» > testo-1.c printf «int main() {\n» >> testo-1.c printf » SomeFunc_1();\n» >> testo-1.c printf «}\n» >> testo-1.c cp testo-1.c testo-2.c $LINK1 testo-1.c -L. -ltesto-1 -o testo-1 $LINK1 testo-1.c -ffunction-sections -fdata-sections -Wl,—gc-sections -L. -ltesto-1f -o testo-1f $LINK1 testo-2.c -L. -ltesto-2 -o testo-2 printf «#include \»testo.h\»\n\n» > testo-3.cpp printf «int main() {\n» >> testo-3.cpp printf » GodClass G;\n» >> testo-3.cpp printf » G.SomeMethod_1();\n» >> testo-3.cpp printf «}\n» >> testo-3.cpp cp testo-3.cpp testo-4.cpp mv testo-3.h testo.h $LINK2 testo-3.cpp -L. -ltesto-3 -o testo-3 $LINK2 testo-3.cpp -ffunction-sections -fdata-sections -Wl,—gc-sections -L. -ltesto-3f -o testo-3f $LINK2 testo-4.cpp -L. -ltesto-4 -o testo-4 rm -f *.c* *.h # анализируем что попало в исполняемые файлы ============== clear echo «Найдено функций в 1-м исполняемом файле: «`nm testo-1 | grep SomeFunc | wc -l` echo «Найдено функций во 2-м исполняемом файле: «`nm testo-2 | grep SomeFunc | wc -l` echo «Найдено методов в 3-м исполняемом файле: «`nm testo-3 | grep SomeMethod | wc -l` echo «Найдено методов в 4-м исполняемом файле: «`nm testo-4 | grep SomeMethod | wc -l` echo «Найдено функций в 1f-м исполняемом файле: «`nm testo-1f | grep SomeFunc | wc -l` echo «Найдено методов в 3f-м исполняемом файле: «`nm testo-3f | grep SomeMethod | wc -l`

Результат работы скрипта таков:

Найдено функций в 1-м исполняемом файле: 20 Найдено функций во 2-м исполняемом файле: 1 Найдено методов в 3-м исполняемом файле: 20 Найдено методов в 4-м исполняемом файле: 1 Найдено функций в 1f-м исполняемом файле: 1 Найдено методов в 3f-м исполняемом файле: 1

Как видно, что если разносить функции и методы в разные единицы трансляции, и результат собирать в библиотеку — то «ненужное» при линковке в исполняемый модуль не попадает.

Соответственно пара вопросов:

  1. Можно ли как-то линкер «упросить» не заносить ненужное в исполняемый файл не используя выше означенный подход?

  2. На сколько применим выше означенный подход для С++ (касаемо удаления реализации неиспользуемых методов класса)?

В данной теме прошу не поднимать вопросы проектирования, вопрос именно по линковке.

Библиотека — это «сборник» кода, который можно многократно использовать в самых разных программах. Как правило, библиотека в языке C++ состоит из 2-х частей:

Заголовочный файл, который объявляет функционал библиотеки.

Предварительно скомпилированный бинарный файл, содержащий реализацию функционала библиотеки.

Некоторые библиотеки могут быть разбиты на несколько файлов и/или иметь несколько заголовочных файлов.

Оглавление:

Типы библиотек

Библиотеки предварительно компилируют по нескольким причинам. Во-первых, их код редко меняется. Было бы напрасной тратой времени повторно компилировать библиотеку каждый раз при её использовании в новой программе. Во-вторых, поскольку весь код предварительно скомпилирован в машинный язык, то это предотвращает получение доступа к исходному коду (и его изменение) сторонними лицами. Этот пункт важен для предприятий/людей, которые не хотят, чтобы результат их труда (исходный код) был доступен всем (интеллектуальная собственность, все дела).

Есть 2 типа библиотек: статические и динамические.

Статическая библиотека (или «архив») состоит из подпрограмм, которые непосредственно компилируются и линкуются с вашей программой. При компиляции программы, которая использует статическую библиотеку, весь функционал статической библиотеки (тот, что использует ваша программа) становится частью вашего исполняемого файла. В Windows статические библиотеки имеют расширение .lib (сокр. от «library»), тогда как в Linux статически библиотеки имеют расширение .a (сокр. от «archive»).

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

Динамическая библиотека (или «общая библиотека») состоит из подпрограмм, которые подгружаются в вашу программу во время её выполнения. При компиляции программы, которая использует динамическую библиотеку, эта библиотека не становится частью вашего исполняемого файла — она ​​так и остается отдельным модулем. В Windows динамические библиотеки имеют расширение .dll (сокр. от «dynamic link library» = «библиотека динамической компоновки»), тогда как в Linux динамические библиотеки имеют расширение .so (сокр. от «shared object» = «общий объект»). Одним из преимуществ динамических библиотек является то, что разные программы могут совместно использовать одну копию динамической библиотеки, что значительно экономит используемое пространство. Еще одним преимуществом динамической библиотеки является то, что её можно обновить ​​до более новой версии без необходимости перекомпиляции всех исполняемых файлов, которые её используют.

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

Библиотека импорта (англ. «import library») — это библиотека, которая автоматизирует процесс подключения и использования динамической библиотеки. В Windows это обычно делается через небольшую статическую библиотеку (.lib) с тем же именем, что и динамическая библиотека (.dll). Статическая библиотека линкуется с вашей программой во время компиляции, и тогда функционал динамической библиотеки может эффективно использоваться в вашей программе, как если бы это была обычная статическая библиотека. В Linux общий объектный файл (с расширением .so) дублируется сразу как динамическая библиотека и библиотека импорта. Большинство линкеров при создании динамической библиотеки автоматически создают к ней библиотеку импорта.

Установка библиотек

Теперь, когда мы уже разобрались с типами библиотек, давайте поговорим о том, как их использовать в наших программах. Установка библиотеки в языке C++ состоит из 4-х последовательных шагов:

Шаг №1: Получите библиотеку. Наилучшим вариантом является найти уже предварительно скомпилированный код (если он вообще существует) под вашу операционную систему, чтобы вам не пришлось компилировать библиотеку самостоятельно. В Windows библиотеки обычно распространяются в виде архивов (файлов .zip), а в Linux это пакеты кода (например, пакеты .rpm).

Шаг №2: Установите библиотеку. В Linux это делается путем вызова менеджера пакетов, а дальше он всё делает сам. В Windows вам придется разархивировать библиотеку самостоятельно в любую выбранную вами папку. Рекомендуется хранить все используемые библиотеки в одном месте для быстрого доступа к ним. Например, создайте папку Libs (C:\Libs) и выделяйте для каждой (используемой вами) библиотеки свою отдельную подпапку.

Шаг №3: Убедитесь, что компилятор знает, где искать заголовочные файлы библиотеки. В Windows это обычно подпапка include внутри основной папки библиотеки (например, если вы установили библиотеку в C:\Libs\SDL-1.2.11, то заголовочные файлы находятся в C:\Libs\SDL-1.2.11\include). В Linux библиотеки обычно устанавливаются в /usr/include. Однако, если файлы установлены в другом месте, вам нужно будет сообщить компилятору, где именно.

Шаг №4: Сообщите линкеру, где искать файлы с реализацией функционала библиотеки. Аналогично с предыдущим шагом, вам нужно указать линкеру место, где находятся файлы с реализацией библиотеки. В Windows это обычно подпапка /lib внутри основной папки библиотеки (C:\Libs\SDL-1.2.11\lib), а в Linux это обычно /usr/lib.

Использование библиотек

Как только библиотека установлена ​​и ваша IDE знает, где искать её файлы, то для того, чтобы вы могли использовать эту библиотеку в ваших проектах, вам необходимо выполнить следующие 3 шага:

Шаг №5: Если вы используете статические библиотеки или библиотеки импорта, сообщите линкеру, какие файлы библиотеки нужно связать с вашей программой.

Шаг №6: Подключите заголовочные файлы библиотеки к вашей программе.

Шаг №7: Если вы используете динамические библиотеки, то убедитесь, что исполняемые файлы будут иметь доступ к файлам библиотеки. Самый простой способ использовать .dll — это скопировать .dll в папку с исполняемым файлом. Поскольку .dll-ка обычно распространяется вместе с исполняемым файлом, то это не составит трудностей.

Шаги №3-№5 включают настройку вашей IDE. К счастью, почти все IDE работают одинаково, когда дело доходит до выполнения подобных задач. На следующем уроке мы рассмотрим, как выполнить данные шаги в Visual Studio.

Всем добрый день!
Представляем вам перевод интересной статьи, который подготовили для вас рамках курса «Разработчик C++». Надеемся, что она будет полезна и интересна для вас, как и нашим слушателям.
Поехали.
Сталкивались ли вы когда-нибудь с терминами внутренняя и внешняя связь? Хотите узнать, для чего используется ключевое слово extern, или как объявление чего-то static влияет на глобальную область? Тогда эта статья для вас.
В двух словах
В единицу трансляции включены файл реализации (.c/.cpp) и все его заголовочные файлы (.h/.hpp). Если внутри единицы трансляции у объекта или функции есть внутреннее связывание, то этот символ виден компоновщику только внутри этой единицы трансляции. Если же у объекта или функции есть внешнее связывание, то компоновщик сможет видеть его при обработке других единиц трансляции. Использование ключевого слова static в глобальном пространстве имен дает символу внутреннее связывание. Ключевое слово extern дает внешнее связывание.
Компилятор по умолчанию дает символам следующие связывания:

  • Non-const глобальные переменные — внешнее связывание;
  • Const глобальные переменные — внутреннее связывание;
  • Функции — внешнее связывание.


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

  • Разница между объявлением и определением;
  • Единицы трансляции.

Также обратите внимание на названия: мы будем использовать понятие «символ”, когда речь идет о любой «сущности кода”, с которой работает компоновщик, например с переменной или функцией (или с классами/структурами, но на них мы не будем акцентироваться).
Объявление VS. Определение
Кратко обсудим разницу между объявлением и определением символа: объявление (или декларация) говорит компилятору о существовании конкретного символа, и позволяет обращение к этому символу в случаях не требующих точного адреса памяти или хранилища символа. Определение говорит компилятору, что содержится в теле функции или сколько памяти нужно выделить переменной.
В некоторых ситуациях компилятору недостаточно объявления, например, когда элемент данных класса имеет тип ссылки или значения (то есть не ссылка, и не указатель). В то же время, разрешен указатель на объявленный (но неопределенный) тип, так как ему нужен фиксированный объем памяти (например, 8 байт в 64-битных системах), не зависящий от типа, на который указывает. Чтобы получить значение по этому указателю, потребуется определение. Также для объявления функции нужно объявить (но не определить) все параметры (не важно взятые ли по значению, ссылке или указателю) и возвращаемый тип. Определение типа возвращаемого значения и параметров необходимо только для определения функции.
Функции
Разница между определением и объявлением функции весьма очевидна.
int f(); // объявление int f() { return 42; } // определение
Переменные
С переменными все немного иначе. Объявление и определение обычно не разделяются. Главное, что это:
int x;
Не только объявляет x, но и определяет его. Происходит это благодаря вызову дефолтного конструктора int. (В C++ в отличие от Java, конструктор простых типов (таких как int) по умолчанию не инициализирует значение в 0. В примере выше х будет иметь равен любому мусору, лежащему в адресе памяти, выделенном компилятором).
Но вы можете явно разделить объявление переменной и ее определение при помощи ключевого слова extern.
extern int x; // объявление int x = 42; // определение
Однако, при инициализации и добавлении extern к объявлению, выражение превращается в определение и ключевое слово extern становится бесполезным.
extern int x = 5; // то же самое, что и int x = 5;
Предварительное Объявление
В C++ существует концепция предварительного объявления символа. Это значит, что мы объявляем тип и имя символа для использования в ситуациях, не требующих его определения. Так нам не понадобится включать полное определение символа (обычно — заголовочный файл) без явной необходимости. Тем самым, мы снижаем зависимость от файла, содержащего определение. Главное преимущество — при изменении файла с определением, файл, где мы предварительно объявляем этот символ, не потребует повторной компиляции (а значит, и все прочие файлы его включающие).

Пример
Предположим, у нас есть объявление функции (называемое прототипом) для f, принимающее объект типа Class по значению:
// file.hpp void f(Class object);
Сразу включить определение Class — наивно. Но так как мы пока только объявили f, достаточно предоставить компилятору объявление Class. Таким образом, компилятор сможет узнать функцию по ее прототипу, а мы сможем избавиться от зависимости file.hpp от файла, содержащего определение Class, скажем class.hpp:
// file.hpp class Class; void f(Class object);
Допустим, file.hpp содержится в 100 других файлах. И, допустим, мы меняем определение Class в class.hpp. Если вы добавим class.hpp в file.hpp, file.hpp и все 100 содержащих его файла будут должны перекомпилироваться. Благодаря предварительному объявления Class единственными файлами, требующими повторной компиляции, будут class.hpp и file.hpp (если считать, что f определен там).
Частота использования
Важное отличие объявления от определения состоит в том, что символ может быть объявлен много раз, но определен только однажды. Так вы можете предварительно объявить функцию или класс сколько угодно раз, но определение может быть только одно. Это называется Правилом Одного Определения. В C++ работает следующее:
int f(); int f(); int f(); int f(); int f(); int f(); int f() { return 5; }
А это не работает:
int f() { return 6; } int f() { return 9; }
Единицы трансляции
Программисты обычно работают с заголовочными файлами и файлами реализации. Но не компиляторы — они работают с единицами трансляции (translation units, кратко — TU), которые иногда называют единицами компиляции. Определение такой единицы довольно простое — любой файл, переданный компилятору, после его предварительной обработки. Если быть точным, это файл, получаемый в результате работы препроцессора расширяющего макрос, включающего исходный код, который зависит от #ifdef и #ifndef выражений, и копипасты всех файлов #include.
Есть следующие файлы:
header.hpp:
#ifndef HEADER_HPP #define HEADER_HPP #define VALUE 5 #ifndef VALUE struct Foo { private: int ryan; }; #endif int strlen(const char* string); #endif /* HEADER_HPP */
program.cpp:
#include «header.hpp» int strlen(const char* string) { int length = 0; while(string) ++length; return length + VALUE; }
Препроцессор выдаст следующую единицу трансляции, которая затем передается компилятору:
int strlen(const char* string); int strlen(const char* string) { int length = 0; while(string) ++length; return length + 5; }
Связи
Обсудив основы, можно приступить к связям. В целом, связь — это видимость символов для компоновщика при обработке файлов. Связь может быть либо внешней, либо внутренней.
Внешняя связь
Когда символ (переменная или функция) обладает внешней связью, он становится видимым компоновщикам из других файлов, то есть «глобально” видимым, доступным всем единицами трансляции. Это значит, что вы должны определить такой символ в конкретном месте одной единицы трансляции, обычно в файле реализации (.c/.cpp), так чтобы у него было только одно видимое определение. Если вы попытаетесь одновременно с объявлением символа выполнить его определение, или поместить определение в файл к объявлению, то рискуете разозлить компоновщик. Попытка добавить файл больше чем в один файл реализации, ведет к добавлению определения больше чем в одну единицу трансляции — ваш компоновщик будет плакать.
Ключевое слово extern в C и C++ (явно) объявляет, что у символа есть внешняя связь.
extern int x; extern void f(const std::string& argument);
Оба символа имеют внешнюю связь. Выше отмечалось, что const глобальные переменные по умолчанию имеют внутреннее связывание, non-const глобальные переменные — внешнее. Это значит, что int x; — то же самое, что и extern int x;, верно? Не совсем. int x; на самом деле аналогичен extern int x{}; (используя синтаксис универсальной/скобочной инициализации, для избежания самого неприятного синтаксического анализа (the most vexing parse)), так как int x; не только объявляет, но и определяет x. Следовательно, не добавить extern к int x; глобально настолько же плохо, как определить переменную при объявлении ее extern:
int x; // то же самое, что и extern int x{}; // скорее всего приведет к ошибке компоновщика. extern int x; // а это только объявляет целочисленную переменную, что нормально
Плохой Пример
Давайте объявим функцию f с внешней связью в file.hpp и там же определим ее:
// file.hpp #ifndef FILE_HPP #define FILE_HPP extern int f(int x); /* … */ int f(int) { return x + 1; } /* … */ #endif /* FILE_HPP */
Обратите внимание, что добавлять здесь extern не нужно, так как все функции явно extern. Разделения объявления и определения тоже не потребуется. Поэтому давайте просто перепишем это следующим образом:
// file.hpp #ifndef FILE_HPP #define FILE_HPP int f(int) { return x + 1; } #endif /* FILE_HPP */
Такой код можно было бы написать до прочтения этой статьи, либо после ее чтения под воздействием алкоголя или тяжелых веществ (например, булочек с корицей).
Давайте посмотрим, почему так делать не стоит. Теперь у нас есть два файла реализации: a.cpp и b.cpp, оба включены в file.hpp:
// a.cpp #include «file.hpp» /* … */
// b.cpp #include «file.hpp» /* … */
Теперь пусть поработает компилятор и сгенерирует две единицы трансляции для двух файлов реализации выше (помните что #include буквально означает копировать/вставить):
// TU A, from a.cpp int f(int) { return x + 1; } /* … */
// TU B, from b.cpp int f(int) { return x + 1; } /* … */
На этом этапе вмешивается компоновщик (связывание происходит после компиляции). Компоновщик берет символ f и ищет определение. Сегодня ему повезло, он находит аж два! Одно в единице трансляции A, другое в B. Компоновщик замирает от счастья и говорит вам примерно следующее:
duplicate symbol __Z1fv in: /path/to/a.o /path/to/b.o
Компоновщик находит два определения для одного символа f. Поскольку у f есть внешнее связывание, он виден компоновщику при обработке и A, и B. Очевидно, это нарушает Правило Одного Определения и вызывает ошибку. Точнее это вызывает ошибку повторяющегося символа (duplicate symbol error), которую вы будете получать не реже, чем ошибку неопределенного символа (undefined symbol error), возникающую, когда вы объявили символ, но забыли определить.
Использование
Стандартным примером объявления переменных extern являются глобальные переменные. Предположим, вы работаете над самовыпекаемым тортом. Наверняка есть глобальные переменные, связанные с тортом, которые должны быть доступны в разных частях вашей программы. Допустим, тактовая частота съедобной схемы внутри вашего торта. Это значение естественно требуется в разных частях для синхронной работы всей шоколадной электроники. (Злой) C-способ объявления такой глобальной переменной имеет вид макроса:

#define CLK 1000000
Программист C++, испытывающий к макросам отвращение, лучше напишет настоящий код. Например такой:
// global.hpp namespace Global { extern unsigned int clock_rate; } // global.cpp namespace Global { unsigned int clock_rate = 1000000; }
(Современный программист C++ захочет использовать разделительные литералы: unsigned int clock_rate = 1’000’000;)
Внутренняя Связь
Если у символа есть внутренняя связь, то он будет виден только внутри текущей единицы трансляции. Не путайте видимость с правами доступа, например private. Видимость означает, что компоновщик сможет использовать этот символ только при обработке единицы трансляции, в которой был объявлен символ, а не позже (как в случае символов с внешней связью). На практике, это значит, что при объявлении символа с внутренней связью в заголовочном файле, каждая единица трансляции, включающая в себя этот файл, получит уникальную копию этого символа. Как если бы вы предопределили каждый такой символ в каждой единице трансляции. Для объектов это значит, что компилятор будет буквально выделять совершенно новую, уникальную копию для каждой единицы трансляции, что, очевидно, может привести к высоким расходам памяти.
Для объявления символа с внутренней связью, в C и C++ существует ключевое слово static. Такое использование отличается от применения static в классах и функциях (или, в целом, в любых блоках).
Пример
Приведем пример:
header.hpp:
static int variable = 42;
file1.hpp:
void function1();
file2.hpp:
void function2();
file1.cpp:
#include «header.hpp» void function1() { variable = 10; }
file2.cpp:
#include «header.hpp» void function2() { variable = 123; }
main.cpp:
#include «header.hpp» #include «file1.hpp» #include «file2.hpp» #include <iostream> auto main() -> int { function1(); function2(); std::cout << variable << std::endl; }
Каждая единица трансляции, включающая header.hpp получает уникальную копию переменной, в силу наличия у нее внутренней связи. Есть три единицы трансляции:

  1. file1.cpp
  2. file2.cpp
  3. main.cpp

При вызове function1 копия переменной file1.cpp получает значение 10. При вызове function2 копия переменной file2.cpp получает значение 123. Однако, значение, которое выдается в main.cpp, не меняется и остается равным 42.
Анонимные пространства имен
В С++ существует другой способ объявления одного и более символов с внутренней связью: анонимные пространства имен. Такое пространство гарантирует, что символы, объявленные внутри него, видны только в текущей единице трансляции. По сути, это просто способ объявить несколько символов static. Какое-то время от использования ключевого слова static в целях объявления символа с внутренней связью отказались в пользу анонимных пространств имен. Однако, им снова стали пользоваться в силу удобства объявления одной переменной или функции с внутренней связью. Есть еще несколько незначительных отличий, на которых я не буду останавливаться.
В любом случае, это:
namespace { int variable = 0; }
Делает (почти) то же самое, что и:
static int variable = 0;
Использование
Так в каких же случаях пользоваться внутренними связями? Использовать их для объектов — плохая идея. Расход памяти больших объектов может быть очень высок из-за копирования под каждую единицу трансляции. Но, в основном, это просто вызывает странное, непредсказуемое поведение. Представьте, что у вас есть синглтон (класс, в котором вы создаете экземпляр только одного инстанса) и неожиданно появляется несколько инстансов вашего «синглтона” (по одному на каждую единицу трансляции).
Однако, внутреннюю связь можно использовать для скрытия из глобальной области локальных хелпер-функций единицы трансляции. Допустим, есть хелпер-функция foo в file1.hpp, которую вы используете в file1.cpp. В то же время у вас есть функция foo в file2.hpp, используемая в file2.cpp. Первая и вторая foo отличаются друг от друга, но вы не можете придумать другие имена. Поэтому вы можете объявить их static. Если вы не будете добавлять и file1.hpp, и file2.hpp в одну и ту же единицу трансляции, то это скроет foo друг от друга. Если этого не сделать, то они будут неявно иметь внешнюю связь и определение первой foo столкнется с определением второй, вызвав ошибку компоновщика о нарушении правила одного определения.