Чому функції C не можна керувати іменами?


136

Нещодавно у мене було інтерв'ю, і одне запитання було в чому полягає в застосуванні extern "C"коду C ++. Я відповів, що це використовувати функції C у коді C ++, оскільки C не використовує керування іменами. Мене запитали, чому C не використовує керування іменами, і якщо чесно, я не зміг відповісти.

Я розумію, що коли компілятор C ++ компілює функції, він дає особливу назву функції головним чином, тому що ми можемо перевантажувати функції одного і того ж імені в C ++, які повинні бути вирішені під час компіляції. У C ім'я функції залишиться незмінним або, можливо, з _ перед ним.

Мій запит: що поганого в тому, щоб дозволити компілятору C ++ також маніпулювати функціями C? Я б припустив, що не має значення, які імена дає їм компілятор. Функції ми називаємо однаково в C і C ++.


75
C не потрібно маніпулювати іменами, оскільки він не має функції перевантаження.
EOF

9
Як зв’язати бібліотеки C з кодом C ++, якщо компілятор C ++ керує іменами функцій?
Мат

6
"Я відповів, що це використовувати функції C у коді C ++, оскільки C не використовує керування іменами." - Я думаю, це навпаки. Зовнішній "C" робить функції C ++ корисними для компілятора C. джерело
rozina

3
@ Engineer999: І якщо ви компілюєте підмножину C, яка також є C ++, за допомогою компілятора C ++, імена функцій дійсно будуть змінені. Але якщо ви хочете мати можливість зв’язати бінарні файли, створені з різними компіляторами, ви не хочете керувати іменами.
EOF

13
C робить MANGLE імена. Зазвичай кепське ім'я - це назва функції, якій передує підкреслення. Іноді це назва функції, за якою слід підкреслювати. extern "C"каже маніпувати ім’я так само, як і "компілятор" C.
Піт Бекер

Відповіді:


187

На це було свого роду відповідь, але я постараюся поставити речі в контекст.

По-перше, С прийшов першим. Таким чином, те, що робить C - це, начебто, "за замовчуванням". Він не манить імена, тому що це просто не так. Ім'я функції - це ім'я функції. Глобал - це глобальний тощо.

Потім прийшов C ++. C ++ хотів мати можливість використовувати той же лінкер, що і C, і мати можливість зв’язатися з кодом, написаним на C. Але C ++ не міг залишити C "маніпулювання" (або його відсутність), як є. Перегляньте наступний приклад:

int function(int a);
int function();

У C ++ це окремі функції, з чіткими тілами. Якщо жоден з них не буде заблокований, обидва будуть називатися "функція" (або "_функція"), і лінкер поскаржиться на перезначення символу. Рішенням C ++ було вписати типи аргументів у ім'я функції. Отже, одне викликається, _function_intа інше викликається _function_void(не реальна схема керування), а зіткнення уникається.

Тепер у нас залишилася проблема. Якщо це int function(int a)було визначено в модулі C, і ми просто беремо його заголовок (тобто декларацію) у код C ++ і використовуємо його, компілятор створить інструкцію лінкеру імпортувати _function_int. Коли функція була визначена, в модулі C це не називалося. Це називалося _function. Це призведе до помилки лінкера.

Щоб уникнути цієї помилки, під час оголошення функції ми повідомляємо компілятору, що це функція, призначена для зв'язку або компіляції компілятором C:

extern "C" int function(int a);

Зараз компілятор C ++ знає, _functionа не імпортувати _function_int, і все добре.


1
@ShacharShamesh: Я вже про це запитав деінде, але як щодо зв’язування в зібраних на C ++ бібліотеках? Коли компілятор перебирає і компілює мій код, який викликає одну з функцій у зібраній бібліотеці C ++, то як він знає, яке ім’я обманювати або надавати функції, лише побачивши його декларацію чи виклик функції? Як знати, що там, де це визначено, його називають чимось іншим? Отже, повинен бути стандартний метод керування іменами в C ++?
Engineer999

2
Кожен компілятор робить це по-своєму. Якщо ви збираєте все з одним компілятором, це не має значення. Але якщо ви спробуєте використати, скажімо, бібліотеку, яка була складена разом із компілятором Borland, з програми, яку ви будуєте за допомогою компілятора Microsoft, ну ... удачі; вам це знадобиться :)
Позначте VY

6
@ Engineer999 Ніколи не замислювалися, чому немає такої речі, як портативні бібліотеки C ++, але вони або вказують, яку саме версію (і прапори) компілятора (і стандартної бібліотеки) ви повинні використовувати, або просто експортуєте API C? Ось так. C ++ - це майже найменш винайдена портативна мова, а C - навпаки. У цьому плані є зусилля, але поки ви хочете чогось справжнього портативного, ви будете дотримуватися C.
Voo

1
@Voo Ну, теоретично, ви повинні мати можливість писати портативний код, просто дотримуючись стандарту, наприклад -std=c++11, і уникати використання нічого, що не відповідає стандарту. Це те саме, що оголосити версію Java (хоча новіші версії Java назад сумісні). Це не стандарти, якими люди користуються, зокрема, для компілятора розширення та залежний від платформи код. З іншого боку, ви не можете їх звинувачувати, оскільки у стандарті бракує багато речей (наприклад, IO, як розетки). Комітет, схоже, повільно досягає цього. Виправте мене, якщо я щось пропустив.
mucaho

14
@mucaho: ви говорите про портативність / сумісність джерела. тобто API. Voo говорить про бінарну сумісність, без повторної компіляції. Для цього потрібна сумісність ABI . Компілятори C ++ регулярно змінюють свій ABI між версіями. (наприклад, g ++ навіть не намагається мати стабільний ABI. Я припускаю, що вони не порушують ABI просто заради задоволення, але вони не уникають змін, які вимагають зміни ABI, коли щось можна отримати і немає іншого хорошого способу зробити це.).
Пітер Кордес

45

Це не те, що вони «не можуть», вони не є , в загальному.

Якщо ви хочете викликати функцію в бібліотеці C, яку називають foo(int x, const char *y), не годиться пускати ваш компілятор C ++, щоб це foo_I_cCP()(або як би там не було, просто склали схему керування на місці тут) лише тому, що це можливо.

Це ім'я не вирішиться, функція знаходиться в C, а її назва не залежить від списку типів аргументів. Тож компілятор C ++ повинен знати це і позначити цю функцію як C, щоб уникнути занять.

Пам'ятайте, що зазначена функція C може бути в бібліотеці, вихідного коду у вас немає, все, що у вас є, є попередньо складеним двійковим кодом та заголовком. Тож ваш компілятор C ++ не може зробити "це власна річ", він не може змінити те, що є в бібліотеці.


Це та частина, яку мені не вистачає. Чому компілятор C ++ маніпулює ім'ям функції, коли він просто бачить декларацію або бачить, що вона викликається. Чи це не просто імена функцій mangle, коли він бачить їх реалізацію? Це мало б більше сенсу для мене
Engineer999

13
@ Engineer999: Як можна вказати одне ім'я для визначення та інше для декларації? "Є функція під назвою Брайан, яку ви можете викликати." "Гаразд, я зателефоную Брайану". "Вибачте, немає функції, яка називається Брайан." Виявляється, це називається Грем.
Гонки легкості по орбіті

А як щодо зв’язування в зібраних на C ++ бібліотеках? Коли компілятор перебирає і компілює наш код, який викликає одну з функцій бібліотеки, складеної на C ++, то як він знає, яке ім'я обманювати або надавати функції, лише побачивши його декларацію чи виклик функції?
Engineer999

1
@ Engineer999 Обидва повинні домовитись про одне й те саме керування. Тож вони бачать заголовочний файл (пам’ятайте, метаданих в рідних DLL дуже мало - заголовки це метадані), і йдеться "Ага, правильно, Брайан справді повинен бути Греймом". Якщо це не працює (наприклад, з двома несумісними схемами керування), ви не отримаєте правильне посилання, і ваша програма не працює. У C ++ є безліч таких несумісностей. На практиці тобі доведеться явно використовувати кепське ім'я та відключити манґлінг на вашій стороні (наприклад, ви кажете своєму коду виконати Грема, а не Брайана). У реальної практиці ... extern "C":)
Luaan

1
@ Engineer999 Можливо, я помиляюся, але у вас, можливо, є досвід роботи з такими мовами, як Visual Basic, C # або Java (або навіть певною мірою Pascal / Delphi)? Зробити інтероп здається надзвичайно простим. У C і особливо в C ++ це все, окрім. Вам потрібно дотримуватися безлічі умовних викликів, ви повинні знати, хто несе відповідальність за пам'ять, і у вас повинні бути файли заголовків, які повідомляють вам декларації функцій, оскільки самі DLL не містять достатньої кількості інформації, особливо у випадку чистий C. Якщо у вас немає файлу заголовка, для його використання, як правило, потрібно декомпілювати DLL.
Луань

32

що не в тому, щоб дозволити компілятору C ++ також переплутати функції C?

Вони більше не будуть функціями C.

Функція - це не просто підпис і визначення; як функціонує функція, багато в чому визначається такими факторами, як виклик конвенції. "Бінарний інтерфейс програми", визначений для використання на вашій платформі, описує, як системи спілкуються один з одним. Використовувана вашою системою AB + C ++ визначає схему керування іменами, щоб програми в цій системі знали, як викликати функції в бібліотеках тощо. (Прочитайте відмінний приклад C ++ Itanium ABI. Ви дуже швидко зрозумієте, для чого це потрібно.)

Те саме стосується і C ABI у вашій системі. Деякі C ABI насправді мають схему керування іменами (наприклад, Visual Studio), тому мова йде менше про "вимкнення керування іменами" та більше про перехід з C ++ ABI на C ABI для певних функцій. Позначимо функції C як функції C, до яких відноситься C ABI (а не C ++ ABI). Декларація повинна відповідати визначенню (будь то в тому ж проекті або в якійсь сторонній бібліотеці), інакше декларація є безглуздою. Без цього ваша система просто не знатиме, як знайти / викликати ці функції.

Що стосується того, чому платформи не визначають CI і C ++ ABI як однакові та позбавляються від цієї "проблеми", це частково історично - оригінальні C ABI були недостатніми для C ++, у якому є простори імен, класи та перевантаження оператора. з яких потрібно якось представити ім'я символу в зручному для комп’ютера способі - але можна також стверджувати, що дотримання програм C сьогодні, дотримуючись C ++, є несправедливим для спільноти C, що повинно було б миритися з значно складнішими ABI лише заради деяких інших людей, які хочуть сумісності.


2
+int(PI/3), але з одним зерном солі: я б дуже обережно говорив про "C ++ ABI" ... AFAIK, є спроби визначення C ++ ABI, але ніяких реальних фактичних / де-юре стандартів - як isocpp.org/files /papers/n4028.pdf заявляє (і я від усієї думки згоден), цитую, глибоко іронічно, що C ++ насправді завжди підтримував спосіб опублікувати API зі стабільним бінарним ABI - вдаючись до підмножини C C ++ через зовнішній “C ”. . C++ Itanium ABIтільки що - деякі C ++ ABI для Itanium ... як обговорювалося на stackoverflow.com/questions/7492180/c-abi-issues-list

3
@vaxquis: Так, не "ABI C ++", а "C ++ ABI" так само, як у мене "ключ від будинку", який працює не в кожному будинку. Здогадайтесь, це могло бути зрозумілішим, хоча я намагався зробити це максимально зрозумілим, починаючи з фрази "АБІ C ++, що використовується вашою системою " . Я викинув освітлювач у пізніших висловлюваннях для стислості, але прийму редагування, яке зменшує плутанину тут!
Гонки легкості по орбіті

1
AIUI C abi, як правило, є власністю платформи, тоді як C ++ ABI, як правило, є власністю окремого компілятора, а часто навіть властивістю окремої версії компілятора. Тож якщо ви хотіли зв’язатись між модулями, побудованими за допомогою різних інструментів постачальника, вам довелося використовувати C abi для інтерфейсу.
підключення

Заява "керовані іменами функції більше не будуть функціями C" є перебільшеною - цілком можливо викликати керовані іменами функції з простої ванілі C, якщо відоме ім'я. Якщо зміна назви не робить його менш прихильним до C ABI, тобто не робить його менш функцією C. Інший спосіб має більше сенсу - C ++ код не міг викликати функцію C, не оголосивши її "C", тому що це дозволило б керувати іменами при спробі встановити зв'язок проти виклику.
Пітер - Відновіть Моніку

@ PeterA.Schneider: Так, заголовна фраза перебільшена. Вся інша частина відповіді містить відповідну фактичну деталь.
Гонки легкості на орбіті

21

MSVC насправді має назву mangle C, хоча просто. Іноді додається @4чи інша невелика кількість. Це стосується умовних вимог і необхідності очищення стека.

Отже, приміщення просто недосконале.


2
Це насправді не назва mangling. Це просто угода про іменування (або ім'я, що прикрашає ім'я), щоб запобігти проблемам з виконуваними файлами, пов'язаними з DLL, побудованими за допомогою функцій, що мають різні умови виклику.
Пітер

2
Що з попередньою формою _?
OrangeDog

12
@Peter: Буквально те саме.
Гонки легкості на орбіті

5
@Frankie_C: "Caller очищує стек" не визначено жодним стандартом C: жодна конвенція про виклик не є більш стандартною, ніж інша з мовної точки зору.
Ben Voigt

2
І з точки зору MSVC, "стандартна конвенція про дзвінки" - саме те, що ви обираєте /Gd, /Gr, /Gv, /Gz. (Тобто використовується стандартна конвенція виклику, якщо функція декларації прямо не визначає умову виклику.) Ви думаєте про те, __cdeclяка стандартна умова виклику за замовчуванням.
MSalters

13

Дуже часто зустрічаються програми, які частково написані на C, а частково написані якоюсь іншою мовою (часто мова асемблера, але іноді Pascal, FORTRAN чи щось інше). Також звичайно, щоб програми містили різні компоненти, написані різними людьми, які можуть не мати вихідного коду для всього.

На більшості платформ є специфікація - часто її називають ABI [Application Binary Interface], яка описує, що повинен робити компілятор, щоб створити функцію з певним іменем, яка приймає аргументи певних типів і повертає значення певного типу. У деяких випадках ABI може визначати кілька "конвенцій, що викликають"; компілятори для таких систем часто надають засоби вказівки, яка умова виклику повинна використовуватися для певної функції. Наприклад, на Macintosh більшість підпрограм Toolbox використовують конвенцію виклику Паскаля, тому прототип для чогось типу "LineTo" був би таким:

/* Note that there are no underscores before the "pascal" keyword because
   the Toolbox was written in the early 1980s, before the Standard and its
   underscore convention were published */
pascal void LineTo(short x, short y);

Якщо весь код у проекті був складений за допомогою одного і того ж компілятора, не було б значення, яке ім'я компілятор експортує для кожної функції, але в багатьох ситуаціях для коду С потрібно буде викликати функції, складені за допомогою інших інструментів і не може бути перекомпільовано з цим компілятором [і, можливо, навіть не буде в C]. Таким чином, можливість визначати ім'я лінкера є критично важливим для використання таких функцій.


Так, це відповідь. Якщо це просто C і C ++, то важко зрозуміти, чому це робиться саме так. Для розуміння ми повинні ставити речі в контекст старого способу статичного зв’язку. Статичні зв'язки здаються примітивними для програмістів Windows, але це головна причина, через яку C не може обробляти імена.
користувач34660

2
@ user34660: Не квіті. Це є причиною того, що C не може доручити існуванню функцій, реалізація яких вимагатиме або керування іменами, які можна експортувати, або дозволити існування декількох символів, подібних до імені, які відрізняються вторинними характеристиками.
supercat

чи знаємо ми, що були спроби "доручити" такі речі чи такі речі були розширеннями, доступними для C до C ++?
користувач34660

@ user34660: Знову "Статичні зв'язки здаються примітивними для програмістів Windows ...", але динамічне посилання іноді здається головним PITA людям, які використовують Linux, якщо встановити програму X (можливо, написану на C ++) означає, що потрібно відстежувати та встановлювати окремі версії бібліотек, у яких у вас вже є різні версії вашої системи.
jamesqf

@jamesqf, так, Unix не мав динамічного посилання перед Windows. Я дуже мало знаю про динамічне підключення в Unix / Linux, але це здається, що воно не настільки безпроблемно, як це могло б бути в операційній системі взагалі.
користувач34660

12

Я додам ще одну відповідь, щоб розглянути деякі тангенціальні дискусії, що відбулися.

C ABI (двійковий інтерфейс програми) спочатку закликав передавати аргументи на стеку у зворотному порядку (тобто - натисканням справа наліво), де абонент також звільняє сховище стека. Сучасний ABI фактично використовує регістри для передачі аргументів, але багато міркувань щодо керування повертаються до передачі цього оригінального аргументу стека.

Оригінальний Pascal ABI, навпаки, відсунув аргументи зліва направо, і виклик повинен був викласти аргументи. Оригінальний C ABI перевершує оригінальний Pascal ABI у двох важливих моментах. Порядок поштовху аргументів означає, що зміщення стеку першого аргументу завжди відомо, дозволяючи функціям, які мають невідому кількість аргументів, де ранні аргументи контролюють, скільки інших аргументів є (алаprintf ).

Другий спосіб перевершити C ABI - це поведінка у випадку, якщо абонент та позивач не погоджуються щодо кількості аргументів. У випадку С, якщо ви фактично не отримаєте доступ до аргументів минулого, нічого поганого не відбувається. У Паскалі неправильна кількість аргументів вискакує зі стека, і вся стека зіпсована.

Оригінальний Windows 3.1 ABI був заснований на Pascal. Як такий, він використовував Паскаль ABI (аргументи в лівому та правому порядку, виклику виклику). Оскільки будь-яка невідповідність аргументації може призвести до корупції, склалася схема керування. Кожне ім'я функції було змінено числом, що вказує на розмір, в байтах, його аргументів. Отже, на 16-бітній машині функція (синтаксис C):

int function(int a)

Був розіграний function@2, бо intшириною два байти. Це було зроблено для того, що якщо невідповідність декларації та визначення, лінкер не зможе знайти функцію, а не пошкодить стек під час виконання. І навпаки, якщо програма посилається, то ви можете бути впевнені, що правильна кількість байтів вискакує зі стека в кінці дзвінка.

32-бітний Windows і далі використовуйте stdcallABI. Він схожий на Pascal ABI, за винятком того, що порядок натискань є як у C, справа наліво. Як і ABC Pascal, ім'я mangling змінює розмір байту аргументів у ім'я функції, щоб уникнути пошкодження стека.

На відміну від претензій, викладених деінде тут, C ABI не манить назви функцій навіть у Visual Studio. І навпаки, функції керування, прикрашені stdcallспецифікацією ABI, не властиві лише VS. GCC також підтримує цей ABI, навіть при компіляції для Linux. Це широко використовується Wine , що використовує власний завантажувач, щоб дозволити час запуску ліній компільованих бінарних файлів Linux до компільованих DLL файлів Windows.


9

Компілятори C ++ використовують керування іменами, щоб дозволити унікальні імена символів для перевантажених функцій, підпис яких інакше був би однаковим. Він в основному кодує і типи аргументів, що дозволяє здійснювати поліморфізм на функціональному рівні.

C цього не вимагає, оскільки не допускає перевантаження функцій.

Зауважте, що керування іменами є однією (але, безумовно, не єдиною!) Причиною того, що не можна покладатися на "C ++ ABI".


8

C ++ хоче мати можливість взаємодіяти з кодом C, який посилається на нього, або проти якого він посилається.

C очікує імен функцій, не керованих іменами.

Якщо C ++ керував ним, він не знайде експортовані некеровані функції з C, або C не знайде експортовані функції C ++. C-лінкер повинен отримати ім'я, яке він очікує сам, оскільки не знає, що він надходить або переходить на C ++.


3

Змінення імен функцій C та змінних дозволить перевірити їх типи в час зв'язку. В даний час всі (?) Реалізації C дозволяють визначити змінну в одному файлі та викликати її як функцію в іншому. Або ви можете оголосити функцію неправильним підписом (наприклад, void fopen(double)а потім викликати її.

Я запропонував схему для безпечного типу взаємозв'язку змінних та функцій C за допомогою використання mangling ще в 1991 році. Ця схема так і не була прийнята, оскільки, як зазначали інші, це знищило б сумісність.


1
Ви маєте на увазі "дозволити перевірку їх типів під час посилання ". Типи які перевіряються під час компіляції, але зв'язок з unmangled імен не може перевірити , є чи згодні заяви , які використовуються в різних одиницях компіляції. І якщо вони не згодні, це ваша система побудови принципово порушена і потребує виправлення.
cmaster - відновити моніку
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.