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

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

// 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
	leal	30(%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

Комментарии (36)


  1. DSarovsky
    16.04.2024 18:19
    +5

    А можно ли было не удалять объявление функции?

    И да, оказалось, что есть второй способ

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

    Кругом обман :(


    1. unreal_undead2
      16.04.2024 18:19

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


  1. Sazonov
    16.04.2024 18:19
    +6

    А потом, когда проект ещё чуть подрастёт и заходят ускорить сборку на билд агентах, начнут использовать unity build и отгребут по полной. На некоторых оно может будет собираться, на других - нет. И отлаживать такое будет крайне неудобно.

    Для си++ такие вещи решаются именованными, а не анонимными неймспейсами.


    1. unreal_undead2
      16.04.2024 18:19

      А можно подробнее? Как анонимный неймспейс для функции, используемой только в одной единице трансляции, помешает билду?


      1. Sazonov
        16.04.2024 18:19

        1. unreal_undead2
          16.04.2024 18:19
          +3

          This is done by creating a (set of) unity sources which #include the original sources

          Спасибо за информацию. Не сталкивался и постараюсь держаться подальше - слишком эффективное средство для стрельбы по ногам (особенно когда в кодовой базе полно legacy и third party).


          1. Sazonov
            16.04.2024 18:19

            Ну если писать на си++ с кучей глобальных функций и переменных в процедурном стиле на уровне джуна - то да. Но в более менее серьезных проектах, unity build - это распространённая практика.


            1. unreal_undead2
              16.04.2024 18:19
              +1

              Навскидку зависимости между third party/системными заголовками (в духе включать winsock2.h строго до windows.h), всяческие макросы влияющие на поведение заголовков типа _POSIX_C_SOURCE тоже приведут к интересным эффектам.


              1. Sazonov
                16.04.2024 18:19

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


                1. unreal_undead2
                  16.04.2024 18:19

                  Если сразу использовать unity build, зная его специфику и явно прописывая исключения - да. Если уже есть десятки мегабайт кода, написанные в разное время разными командами - то использовать опасно. Хорошо ещё, если код просто иногда не билдится - такое можно быстро обнаружить и пофиксить, а вот если билдится всегда, но в зависимости от объединения файлов меняется семантика работы - тут похуже.


                  1. Sazonov
                    16.04.2024 18:19

                    Ничего опасного, не придумывайте. Я год назад добавлял такую фичу в кросс-платформенный проект. Не скажу что сильно большой, примерно 5 лет командой из 6-8 человек делался. Со всеми код ревью, доработкой документации по кодстайлу и тестированием ушло 3 дня.


  1. ReadOnlySadUser
    16.04.2024 18:19
    +11

    Никогда бы не подумал, что прочитаю такую простыню о такой наитупейшей вещи как static функции :)

    Вот вас ждут еще открытия, когда вы для себя откроете inline :)


  1. boldape
    16.04.2024 18:19
    +1

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

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


    1. eao197
      16.04.2024 18:19
      +4

      Но давайте признаем, что виноват на самом деле страус, изобретая с++ он почему то решил что модули это для слабоков

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


      1. boldape
        16.04.2024 18:19

        Да, и т.е. спустя сколько там 40 лет комитет таки запили модули и ничего не отвалилось. Мало того он и сам из хотел сделать, но типа и так пойдет. Си здесь вообще не причем.


        1. eao197
          16.04.2024 18:19
          +1

          Да, и т.е. спустя сколько там 40 лет комитет таки запили модули и ничего не отвалилось.

          Об этом рано говорить, модули пока что используют не только лишь все.

          Мало того он и сам из хотел сделать, но типа и так пойдет.

          Он и auto изначально хотел использовать для автоматического вывода типов. Но "совместимость" с Си не позволила.

          Си здесь вообще не причем.

          Ну да, ну да. Взлетел бы C++ если бы он бы не помесью Симула и Си, а помесью Симула и Модула-2 (к примеру) -- большой вопрос. По факту же C++ смог несколько десятилетий жить за счет экосистемы Си (начиная от линкеров и форматов объектных файлов, заканчивая бесплатной интеграцией с Си-шным кодом). Как-то странно слышать, что "Си здесь вообще ни при чем".


          1. boldape
            16.04.2024 18:19

            Конечно не причем, наймспэйсы то были, да и вообще свой не совместимый с си манглин имён с самого начала. Модули в этом плане очень рядом конкретно Си никак немашал их запилить ещё тогда.


            1. eao197
              16.04.2024 18:19

              Конечно не причем, наймспэйсы то были

              Изначально их не было. Они появились в C++ только в 1990-ом году, когда язык уже стал популярным за счет своей совместимости с Си.

              Более того, шаблонов в C++ так же изначально не было. И выкручиваться с обобщенным программированием на C++ тогда приходилось за счет Си-шного препроцессора с define-ами.


              1. boldape
                16.04.2024 18:19
                +1

                Ну я историю с++ так детально не знаю, но период до шаблонов вообще можно не считать потому что это не с++, а фигня какая-то, которой пользовались 3 калеки.

                Так историческая справка, забавно что первый раз про с++ я узнал из та-дам телевизора в 98 году, из новости что наконец то родили ёжика, с++98. Помню как показывали радующегося страуса. Я тогда из программирования мог только - да ничего толком.


                1. unreal_undead2
                  16.04.2024 18:19

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


                1. eao197
                  16.04.2024 18:19

                  Ну я историю с++ так детально не знаю

                  Для вас это всего лишь история, для меня -- собственный опыт.

                  но период до шаблонов вообще можно не считать потому что это не с++, а фигня какая-то, которой пользовались 3 калеки.

                  Чуть больше, по некоторым оценкам количество программистов на C++ в 1991-ом году оценивалось где-то в 400K.

                  Ну и эта самая "фигня какая-то" была сильно круче тогдашних конкурентов (коих и не сказать, чтобы было много на конец 1980-х и начало 1990-х).

                  Забавно, что для некоторых C++ в то время не был отдельным языком программирования, а воспринимался просто как следующая версия Си, в который добавили пару-тройку новых ключевых слов. Тем более, что одним из "selling points" C++ всегда было то, что можно было взять уже имеющийся код на Си и с минимальными переделками начать использовать его как код на C++ (собственно перевод кодовой базы GCC на C++ как раз лишний тому пример). Даже без классов и перегрузки операторов. Просто ссылки вместо указателей.

                  Так что если бы C++ не выглядел в начале своей жизни именно как нашлепка над Си, то вряд ли он обрел бы такую популярность. Мог бы запросто повторить судьбу Eiffel-я, а то и Modula-3 (при том, что Eiffel не умер, живет себе и здравствует, просто мало кому нужен).


                  1. boldape
                    16.04.2024 18:19

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

                    Что ещё? Деструкторы в в частности и раи в общем. К деструкторам у меня лично есть претензии, но раи это сила да.

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

                    Семантика копирования по умолчанию - явно проигрышная идея.

                    Наверное ещё эксепции были, я не знаю точно. Мне эксепции нравятся, но я понял что спустя сколько там 40 лет уже большая часть плюсистов НЕ понимает этот инструмент и это (а не пресловутая производительность) является главным мотивом их не использовать.

                    Ссылки появились только ради операторов и не доделанные они до сих пор. Что в них хорошо это то что они константность переносят на сам объект и всегда не нул.

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

                    Т.е. безусловно хорошая и новая фича по сравнению с си только раи.

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


  1. DungeonLords
    16.04.2024 18:19

    Простите за оффтоп, но можно привести примеры когда нужно и когда не нужно писать static inline к строке типа
    static inline constexpr uint8_t i= 1;
    Я унаследовал кодовую базу C и перевожу ее на C++. Соответственно заменяю #define на static inline constexpr. Нужно ли тут писать static inline?


    1. boldape
      16.04.2024 18:19
      +1

      Короткий ответ - inline constexpr

      Почему?

      constexpr означает const. Конст для переменных объявленных в глобальном скоупе (а именно там обычно дефайнят константы в си) подразумевает static. Поэтому constexpr и static constexpr для переменных в глобальном скоупе это синонимы.

      НО статик означает интернал линкэдж, это значит что в каждом цпп файле где инклудится хидер с глобальным констэкспром МОЖЕТ (но не обязан) иметь отдельный сторэдж под эту переменную, что может привести к росту размера бинарника. Если у такой переменной берется адрес, то тогда ТОЧНО будет выделен отдельный сторэдж в каждом цппшнике где берется адрес.

      Зачем нужен инлайн? Инлайн означает экстернал линкэдж, это значит что ВСЕ переменные обязаны иметь ровно один сторэдж в независимости от того сколько цппшников берет адрес. Это может не позволить расти размеру бинарника.



  1. LifeKILLED
    16.04.2024 18:19

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


  1. unreal_undead2
    16.04.2024 18:19
    +1

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

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


  1. 9241304
    16.04.2024 18:19
    +1

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


    1. KudryashovDA Автор
      16.04.2024 18:19

      Спасибо. Да, примеры в рассказах много значат, наверное можно было бы придумать лучше. В процессе написания они несколько раз переписывались, пока не остановился на некотором компромисе.
      Мне сильно подходит способ изучения языка через решение проблемы. Так более детально можно погрузиться в конкретную область, а таких областей в C++ предостаточно. Чуть выше @Playa привел ссылку на прекрасный материал по спецификаторам, спасибо большое. Она мне не раз попадалась на глаза. Только она написана как справочник с большим объемом информации. За одно прочтение думаю мало кто осилит все переварить и сразу использовать в работе. Можно к ней обращаться когда уже немного в теме и набил шишек на собственных ошибках.
      И да, эта статья изначально задумывалась как развлекательная с элементами теории и практики. Для себя хотелось бы на Хабре видеть больше материала в таком стиле.


  1. bar2104
    16.04.2024 18:19
    +1

    А просто использовать namespace слишком сложно? Или согласовать имена функций?


    1. KudryashovDA Автор
      16.04.2024 18:19
      +1

      Или согласовать имена функций?

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


  1. coodi
    16.04.2024 18:19

    А представляете, как в си без namespace страдают :) шучу. На самом деле такие конфликты спокойно решаются внутренним гайдлайном, в котором расписано всё, вплоть до названий функций и стиля их написания. Ну вообще тут конечно представлен пример странного с++


  1. AndronNSK
    16.04.2024 18:19

    Однажды мне потребовалось включить проект 2 библиотеки на Си, написанные одним человеком и делающие однотипные вещи, но для разных стандартов.

    Естественно, тот человек называл интерфейсные функции одинаково.

    Как С++ тот код не скомпилировался, чтобы обернуть в неймспейс.

    Пришлось загружать их динамически и работать с ними по-очереди.


    1. yatanai
      16.04.2024 18:19

      Вроде же можно включать библиотеки из С в отдельный неймспейс, не?

      extern "C" namespace abc {
        #include <clib>
      }


      1. AndronNSK
        16.04.2024 18:19

        Я не знаком с таким синтаксисом, но с виду он не решает проблему линковки дублирующихся символов.


    1. ReadOnlySadUser
      16.04.2024 18:19

      Пришлось загружать их динамически и работать с ними по-очереди.

      Ну, можно было бы и работать вместе. Через тот же dlsym (или его виндовый аналог GetProcAddress) можно найти нужный символ в каждой отдельной библиотеке.

      В случае GCС / Clang можно было бы изобрести костыль c помощью -Wl,--version-script, если позарез нужна статическая линковка. С MSVC пришлось бы пострадать)