Сказание о static и неименованном пространстве имен для функции в C++

—

от автора

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

Итак, первая команда героически напряглась и дала миру следующий код:

// parser.h #pragma once  int process_data(int); int parse_data(int);
// parser.cpp #include "parser.h"  int process_data(int raw_data) { return 15; }  int parse_data(int raw_data) { return process_data(raw_data); }
// main.cpp #include "parser.h"  int main() { const int raw_data = 1; const int data = parse_data(raw_data); return data; }

И отправила его в master. Компиляция на сервере прошла успешно и ребята с чувством выполненного долга отправились вместе с их руководителем в бар.

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

// solver.h #pragma once  int process_data(int); int solve(int);
// solver.cpp #include "solver.h"  int process_data(int temp_data) { // - Петька, приборы! - 30! - что 30? - А что "приборы"? return temp_data + 30; }  int solve(int data) { return process_data(data); }

Ребята знали, что первая команда уже написала заготовку кода в main.cpp, поэтому они просто сделали git pull и внесли кое-какие коррективы в код, чтобы активировать новый функционал:

// main.cpp #include "parser.h" ++#include "solver.h"  int main() { const int raw_data = 1; const int data = parse_data(raw_data); --return data; ++return solve(data); }

Так сложилось, что в отделе разработки все очень любят использовать слово «process». И в этот раз так получилось, что вторая команда придумала название вспомогательной функции в точности совпадающее с той, что реализовала первая команда, но еще не знала об этом.

При попытке собрать новый код любимая IDE внезапно выдала грозное сообщение об ошибке:

multiple definition of `process_data(int)';

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

Начали выяснять. Самый молодой из них метко заметил, что в файле solver.cpp действительно два раза встречается упоминание «process_data», о чем и как бы и говорит ошибка:

int -->process_data<--(int temp_data) { return temp_data + 30; }  int solve(int data) { return -->process_data<--(data); }

Но тот, что по-опытнее ему аргументированно ответил: — «Не, мы так и раньше делали, ошибок не возникало».

Тот, что самый опытный из команды (дальше по тексту «Герой») решил более внимательно изучить сообщение об ошибке, полный текст которого представлен ниже:

/usr/bin/ld: /tmp/ccOIubul.o: in function `process_data(int)': solver.cpp:(.text+0x1b): multiple definition of `process_data(int)'; /tmp/ccelFhGq.o:parser.cpp:(.text+0x1b): first defined here collect2: error: ld returned 1 exit status

и увидел в нем упоминание файла, созданного другой командой — parser.cpp, а фраза «first defined here» как бы намекала, что та команда первой там что-то определила.
Казалось вот-вот решение найдется. Самое простое, что можно было сделать, это сказать руководителю, что у нас все хорошо, локально все собиралось, пока не смержили с кодом, сделанным другой командой, это наверняка они что-то сделали не так. Но на отправленные руководителю сообщения никто так и ответил, видимо классно проводят время в баре, подумал Штирлиц герой.

Делать все равно что-то нужно, стали искать помощи в интернете.
Первая ссылка по запросу «multiple definition of» привела на переведенный на русский язык совет на SO:

Исходник, в котором определяется XYZArray подключается два раза. Не следует подключать CPP-файлы через include, а в H-файлах должна быть проверка на повторное включение.

Проверка на повторное чтение есть, через инклюд подключаются только h-файлы. Неужели pragma не сработала? Слышал же где-то, что лучше писать через define. Переписали код на вариант с #ifndef/#define — не помогло.

В другом совете речь шла про какое-то связывание и статик. Чутье нашего героя посоветовало копать в эту сторону. Неспроста его сегодня утром сильно ударило статическим электричеством когда он потянулся перетыкать usb мышку. Дело в том, что кресло у товарища сделано из синтетики, как и штаны героя. Но он об этом пока не догадывается и списывает все на начавшиеся проявляться долгожданные сверхспособности.

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

Как выяснилось, никто из команды раньше не задумывался как из многочисленных h- и cpp-файлов после компиляции получается один бинарный файл программы. За них все делала их любимая IDE, где пишешь код, жмешь на кнопочку и если повезет, то она не выдаст ни одной ошибки. Еще ребята краем уха слышали, что в другой команде пишут какие-то тесты к коду и вроде как им меньше прилетает от тестировщиков. Но это так, к слову.

Так, вот гуглеж показал, что когда вы вызываете например команду:

g++ main.cpp parser.cpp solver.cpp

за кадром происходит много всего, в том числе некое связывание, а тот кто связывает — зовется линкером и он оповестил нас об ошибке:

/usr/bin/ld: ...multiple definition of ...

Вроде бы мы на верном пути, осталось понять причем тут static и что все-таки связывается.

Оказалось, что cpp-файлы из команды которая приведена выше на самом деле компилируются по-отдельности в т.н. объектные файлы — созданные ассемблером промежуточные файлы, хранящие куски машинного кода [1]. Это подтверждают и записи в сообщении об ошибке:

... /tmp/ccOIubul.o ... /tmp/ccelFhGq.o

Машинный код читать неинтересно, а ассемблерный для наших исходников выглядит так (точнее его часть):

g++ -S -O2 parser.cpp  .file"parser.cpp" .text .p2align 4 .globl_Z12process_datai .type_Z12process_datai, @function _Z12process_datai: .LFB3: .cfi_startproc endbr64 movl$15, %eax ret .cfi_endproc ...
g++ -S -O2 solver.cpp  .file"solver.cpp" .text .p2align 4 .globl_Z12process_datai .type_Z12process_datai, @function _Z12process_datai: .LFB0: .cfi_startproc endbr64 leal30(%rdi), %eax ret .cfi_endproc ...

Что мы видим? На первый взгляд ничего не понятно, но если присмотреться, то можно найти одинаковые названия в обоих листингах:

.globl_Z12process_datai ... _Z12process_datai:

_Z12process_datai до боли напоминает нашу несчастную функцию, которая фигурировала в сообщении об ошибке. Только у нее еще кое-что прилеплено спереди и сзади. То, что сзади, кстати очень похоже на тип аргумента функции — int. А некая метка .globl созвучна с чем-то глобальным, типа глобальная переменная, но видимо и к функциям оно тоже применимо.

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

Проблему локализовали, теперь нужно найти решение.

Раз мы уже видели ассемблерный код, то первое что приходит в голову, это изменить ту странную сигнатуру функции в нашем объектном файле либо подшаманить с меткой .globl.

На форумах пишут, что ключевое слово static для свободных функций как-то скрывает область видимости функции внутри объектного файла. Попробуем изменить solver.h:

--int process_data(int); ++static int process_data(int);

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

g++ -S -O0 solver.cpp  .file"solver.cpp" .text .type_ZL12process_datai, @function _ZL12process_datai: .LFB0: .cfi_startproc endbr64 .... _Z5solvei: .LFB1: .cfi_startproc endbr64 pushq%rbp ....

И о чудо! В название функции добавилась буква L: _ZL12process_datai да и метка .globl исчезла! Вот оно счастье!

Но радость оказалась преждевременной. При попытке скомпилировать весь проект компилятор отрапортовал новой ошибкой:

g++ main.cpp parser.cpp solver.cpp   In file included from main.cpp:2: solver.h:3:12: error: ‘int process_data(int)’ was declared ‘extern’ and later ‘static’ [-fpermissive]     3 | static int process_data(int);       |            ^~~~~~~~~~~~ In file included from main.cpp:1: parser.h:4:5: note: previous declaration of ‘int process_data(int)’     4 | int process_data(int);

Сообщение

was declared ‘extern’ and later ‘static’

оказалось довольно часто встречаемым в интернете, но все ссылки уходили во времена чистого С, с которым наш герой не хотел разбираться. Поэтому он принял импульсивное решение — удалить из своего h-файла объявление функции int process_data(int), а static приписать к ее определению в cpp-файле:

--int process_data(int temp_data) { ++static int process_data(int temp_data) {

И сработало! Проект скомпилировался. Можно было бы радоваться, но что-то не давало покоя нашему герою.

А можно ли было не удалять объявление функции? А можно как-то по-другому ограничить область видимости функции? Что за extern? Почему много отсыла на язык С?

И да, оказалось, что есть второй способ скрыть функцию — использовать неименованное пространство имен или unnamed namespace по ихнему. Кстати, именно его рекомендуют в неких Core C++ Guidelines [2].

Чтобы это провернуть понадобилось обернуть определение функции в конструкцию вида namespace { ... }:

namespace {  int process_data(int temp_data) { return temp_data + 30; }  }

Правда в заголовочном файле все-таки пришлось убрать ее объявление, т.к. выскакивала ошибка про ambiguous.

Проверка ассемблерного кода отдельно скомпилированного solver.cpp показала, что имя функции действительно поменялось:

.file"solver.cpp" .text .type_ZN12_GLOBAL__N_112process_dataEi, @function _ZN12_GLOBAL__N_112process_dataEi: .LFB0: .cfi_startproc endbr64 .....

Весь проект также успешно скомпилировался. Наш герой решил, что на этом можно остановиться и залить все в мастер, как тут пришел долгожданный ответ от начальника:

Ребят, хорош там фигней страдать, просто переименуйте вашу функцию и давайте к нам в бар.

Так они и сделали, а потом на посиделках часто вспоминали этот случай.

Ссылки:
[1] https://habr.com/ru/articles/478124/
[2] https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#sf22-use-an-unnamed-anonymous-namespace-for-all-internalnon-exported-entities


ссылка на оригинал статьи https://habr.com/ru/articles/808197/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *