Линковка constexpr с другими TU



Поступил в личку запрос от подписчика Сергея на пост по поводу линковки constexpr функций, которые используют статические переменные, с другими единицами трансляции. Чтож, будем рассказывать.



Для начала вспомним, что какое влияние ключевое слово constexpr оказывает на функции в плане линковки. Для функций constexpr подразумевает inline, поэтому мы уходит от первоначального вопроса к вопросу использования статических переменных в inline функциях.



Использование может быть разным. Сегодня рассмотрим использование статических констант в непосредственно в теле функции.



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



inline подразумевает внешнюю линковку. То есть другие единицы трансляции спокойно могут видеть определение сущности и взаимодействовать с ним. inline сущности могут иметь несколько определений в разных единицах трансляции. А компановщик после компиляции в итоговом бинарнике оставляет из всех лишь одно определение inline сущности.



Статические же сущности уникальны для каждой единицы компиляции и никому не позволительно иметь к ним доступ при линковке. Эдакие эндемики своей TU.



Еще инлайн функции имеют свойство иногда встраиваться в код caller'а. В случае, если в данной TU встроены все вызовы функции, то компилятор на оптимизациях может разрешить себе вообще не генерировать никакого определения.



И тут мы приходим в первой ситуации: мы определили inline функцию в одной единице трансляции и пытаемся из другой единицы получить к ней доступ. Условно так:



//first.cpp

static const int a = 3;

constexpr int gaga() {

return a;

}



//second.cpp

int gaga();

void boo() {

gaga();

}





Это дело в таком виде не соберется даже без оптимизаций. Функция boo будет отсылаться на несуществующий символ gaga. Можно провести ряд манипуляций, чтобы в таком виде генерировалось определение, но на оптимизациях компилятор все равно его выкинет и сборка зафейлится.



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



А они есть.



Более подходящим и общеиспользуемым вариантом организации кода с inline сущностями является помещение их в хэдэры и подключение в те TU, где они будут использоваться. Выглядит это примерно так:



//header.hpp

static const int const_var = 3;

constexpr int gaga() {

return const_var;

}

//first.cpp

#include "header.hpp"

void boo() {

gaga();

}



//second.cpp

#include "header.hpp"

void kak_delaut_gucy() {

gaga();

}





Этот чудокод теперь собирается без проблем, компилятор встроит все вызовы и будет все хорошо. Но вот что будет, если функция gaga будет чуть сложнее для того, чтобы ее встраивать? Что будет, если для first.cpp и second.cpp компилятор все-таки будет генерировать определение gaga?



А будет UB. Тут применимо вот такое правило.

If an inline function [...] with external 

linkage is defined differently in

different translation units,

the behavior is undefined.



Но почему же определения разные? Мы же один и тот же код с одной и той же константой просто копируем в нужные единицы трансляции.



Только вот константы на самом деле разные. В каждой единице трансляции будет своя копия const_var и каждое определение gaga будет ссылаться на разные сущности-копии const_var.



В итоге останется одно определение функции, которое будет в себе содержать ссылку на локальную для единицы трансляции сущность. И любая другая единица трансляции может получается получить доступ к этой локальной сущности. Не уверен, что это вообще по-христиански.



Конечно, компилятор скорее всего оптимизирует использование такой простой переменной и все будет работать как ожидается. Но просто сам формат организации кода и зависимостей сущностей может привести к UB. Оно вам надо? Оно вам не надо.



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