Функції, що підставляються (вбудовуються). Перевантаження функцій

Перевантаження функцій

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

Термінологія

Термін "перевантаження" - це калька англійського "overloading", що з'явилася в російських перекладах книг з мов програмування у першій половині 1990-х років. Можливо, це не самий кращий варіантперекладу, оскільки слово «перевантаження» у російській має усталене власне значення, що кардинально відрізняється від знову запропонованого, проте, він прижився і поширений досить широко. У виданнях радянських часів аналогічні механізми називалися російською «перевизначенням» чи «повторним визначенням» операцій, але цей варіант небезперечний: виникають різночитання і плутанина в перекладах англійських «override», «overload» і «redefine».

Причини появи

У більшості ранніх мов програмування існувало обмеження, згідно з яким одночасно в програмі не може бути доступно більше однієї операції з тим самим ім'ям. Відповідно, всі функції та процедури, видимі в цій точці програми, повинні мати різні імена. Імена та позначення функцій, процедур та операторів, що є частиною мови програмування, не можуть бути використані програмістом для власних функцій, процедур та операторів. У деяких випадках програміст може створити власний програмний об'єкт з ім'ям іншого, вже наявного, але тоді новостворений об'єкт «перекриває» попередній, і одночасно використовувати обидва варіанти стає неможливо.

Подібне становище незручно в деяких випадках, що досить часто зустрічаються.

  • Іноді виникає потреба описувати та застосовувати до створених програмістом типів даних операції, за змістом еквівалентні вже наявним у мові. Класичний приклад – бібліотека для роботи з комплексними числами. Вони, як і звичайні числові типи, підтримують арифметичні операції, і природним було б створити даного типуоперації «плюс», «мінус», «помножити», «розділити», позначивши їх тими самими знаками операцій, що й інших числових типів. Заборона використання певних у мові елементів змушує створювати безліч функцій з іменами виду ComplexPlusComplex, IntegerPlusComplex, ComplexMinusFloat тощо.
  • Коли однакові за змістом операції застосовуються до операндів різних типів, їх доводиться називати по-різному. Неможливість застосовувати для різних типівфункції з одним ім'ям призводить до необхідності вигадувати різні імена для того самого, що створює плутанину, а може і призводити до помилок. Наприклад, у класичній мові Сі існує два варіанти стандартної бібліотечної функції знаходження модуля числа: abs() і fabs() – перший призначений для цілого аргументу, другий – для речового. Таке положення, у поєднанні зі слабким контролем типів Сі, може призвести до помилки, що важко виявити: якщо програміст напише в обчисленні abs(x), де x - речовинна змінна, то деякі компілятори без попереджень згенерують код, який буде перетворювати x до цілого шляхом відкидання дробової частини та обчислювати модуль від отриманого цілого числа!

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

Саме по собі перевантаження операцій - всього лише «синтаксичний цукор», хоча навіть у такій якості вона може бути корисною, тому що вона дозволяє розробнику програмувати більш природним чином і робить поведінку типів користувача більш схожою на поведінку вбудованих. Якщо ж підійти до питання з більш загальних позицій, то можна помітити, що засоби, що дозволяють розширювати мову, доповнювати її новими операціями та синтаксичними конструкціями (а перевантаження операцій є одним із таких засобів, поряд з об'єктами, макрокомандами, функціоналами, замиканнями) перетворюють його вже в метамова - засіб опису мов, орієнтованих на конкретні завдання. З його допомогою можна для кожної конкретної задачі побудувати мовне розширення, найбільш відповідне їй, яке дозволить описувати її рішення в найбільш природній, зрозумілій і простій формі. Наприклад, у додатку до перевантаження операцій: створення бібліотеки складних математичних типів (вектори, матриці) та опис операцій з ними у природній, «математичній» формі, створює «мова для векторних операцій», в якій складність обчислень прихована, і можливо описувати вирішення завдань у термінах векторних та матричних операцій, концентруючись на суті завдання, а не на техніці. Саме з цих міркувань подібні засоби були свого часу включені в мову Алгол-68.

Механізм навантаження

Реалізація

Перевантаження операцій передбачає введення в мову двох взаємопов'язаних особливостей: можливості оголошувати в одній області видимості кілька процедур або функцій з однаковими іменами та можливості описувати власні реалізації операцій (тобто знаків операцій, які зазвичай записуються в інфікній нотації, між операндами). Принципово реалізація їх досить проста:

  • Щоб дозволити існування кількох однойменних операцій, достатньо ввести в мову правило, згідно з яким операція (процедура, функція або оператор) упізнаються компілятором не тільки за назвою (позначенням), але і за типами їх параметрів. Таким чином, abs(i), де i оголошено як ціле, і abs(x), де x оголошено як речове - це дві різні операції. Принципово у забезпеченні саме такого трактування немає жодних складнощів.
  • Щоб дати можливість визначати та перевизначати операції, необхідно ввести в мову відповідні синтаксичні конструкції. Варіантів їх може бути досить багато, але, по суті, вони нічим один від одного не відрізняються, достатньо пам'ятати, що запис виду<операнд1> <знакОперации> <операнд2>» принципово аналогічна виклику функції «<знакОперации>(<операнд1>,<операнд2>)». Досить дозволити програмісту описувати поведінку операторів як функцій - і проблема опису вирішена.

Варіанти та проблеми

Перевантаження процедур та функцій на рівні спільної ідеї, Як правило, не становить складності ні в реалізації, ні в розумінні. Однак навіть у ній є деякі «підводні камені», які необхідно враховувати. Дозвіл навантаження операцій створює набагато більше проблем як для реалізатора мови, так і для працюючого цією мовою програміста.

Проблема ідентифікації

Перше питання, з яким стикається розробник транслятора мови, що дозволяє перевантаження процедур та функцій: яким чином з однойменних процедур вибрати ту, яка має бути застосована в даному конкретному випадку? Все добре, якщо існує варіант процедури, типи формальних параметрів якого точно збігаються з типами параметрів фактичних, застосованих у цьому виклику. Однак практично у всіх мовах у вживанні типів існує певний ступінь свободи, що передбачає, що компілятор у певних ситуаціях автоматично виконує безпечні перетворення типів. Наприклад, в арифметичних операціях над речовим та цілим аргументами цілий зазвичай приводиться до речового типу автоматично, і результат виходить речовим. Припустимо, що є два варіанти функції add:

Int add(int a1, int a2); float add(float a1, float a2);

Як компілятор повинен обробити вираз y = add(x, i) , де x має тип float, а i - тип int? Очевидно, що точного збігу немає. Є два варіанти: або y=add_int((int)x,i) , або як y=add_flt(x, (float)i) (тут іменами add_int і add_float позначені відповідно перший і другий варіанти функції).

Виникає питання: чи повинен транслятор дозволяти подібне використання перевантажених функцій, а якщо повинен, то на якій підставі він вибиратиме конкретний варіант? Зокрема, у наведеному вище прикладі, чи транслятор при виборі повинен враховувати тип змінної y? Потрібно відзначити, що наведена ситуація - найпростіша, можливі набагато заплутаніші випадки, які посилюються тим, що не тільки вбудовані типи можуть перетворюватися за правилами мови, але й оголошені програмістом класи за наявності у них родинних відносин допускають приведення один до одного. Вирішень у цієї проблеми два:

  • Заборонити неточну ідентифікацію загалом. Вимагати, щоб для кожної конкретної пари типів існував точно підходящий варіант перевантаженої процедури або операції. Якщо такого варіанта немає, то транслятор повинен видавати помилку. У цьому випадку програміст повинен застосувати явне перетворення, щоб привести фактичні параметри до потрібного набору типів. Цей підхід незручний у мовах типу C++, що допускають достатню свободу у поводженні з типами, оскільки він призводить до істотної відмінності поведінки вбудованих та перевантажених операцій (до звичайних чисел арифметичні операції можна застосовувати, не замислюючись, а до інших типів - тільки з явним перетворенням) або до появи величезної кількості варіантів операцій.
  • Встановити певні правила вибору «найближчого варіанту». Зазвичай у цьому варіанті компілятор вибирає ті з варіантів, виклики яких можна отримати з вихідного лише безпечними (не призводять до втрати інформації) перетвореннями типів, а якщо їх кілька - може вибирати, виходячи з того, який варіант вимагає менше таких перетворень. Якщо в результаті залишається кілька можливостей, компілятор видає помилку та вимагає явної вказівки варіанта від програміста.

Специфічні питання перевантаження операцій

На відміну від процедур та функцій, інфіксні операції мов програмування мають дві додаткові властивості, що істотно впливають на їх функціональність: пріоритет та асоціативність, наявність яких обумовлюється можливістю «ланцюжкового» запису операторів (як розуміти a+b*c: як (a+b )*c або як a+(b*c) ?Вираз a-b+c - це (a-b)+c або a-(b+c) ?).

Вбудовані в мову операції завжди мають наперед задані традиційні пріоритети та асоціативність. Виникає питання: які пріоритети та асоціативність матимуть перевизначені версії цих операцій чи, тим більше, нові створені програмістом операції? Є й інші тонкощі, які можуть потребувати уточнення. Наприклад, у Сі існують дві форми операцій збільшення та зменшення значення ++ і -- - префіксна та постфіксна, поведінка яких різниться. Як повинні поводитися перевантажені версії таких операцій?

Різні мови по-різному вирішують наведені питання. Так, у C++ пріоритет та асоціативність перевантажених версій операцій зберігаються такими ж, як і у визначених у мові; перевантажити окремо префіксну та постфіксну форму операторів інкременту та декреманту можливо, використовуючи спеціальні сигнатури:

Таким чином, int використовується для внесення відмінності у сигнатури

Оголошення нових операцій

Ще складніше справа з оголошенням нових операцій. Включити в мову саму можливість такого оголошення нескладно, але реалізація його пов'язана зі значними труднощами. Оголошення нової операції - це, фактично, створення нового ключового словамови програмування, ускладнене тим, що операції у тексті, зазвичай, можуть йти без роздільників коїться з іншими лексемами. З появою виникають додаткові проблеми, у організації лексичного аналізатора. Наприклад, якщо в мові вже є операції «+» та унарний «-» (зміна знака), то вираз a+-b можна безпомилково трактувати як a + (-b) , але якщо в програмі оголошується нова операція +-, відразу виникає неоднозначність, адже той самий вираз можна вже розібрати і як a (+-) b . Розробник та реалізатор мови має якимось чином вирішувати подібні проблеми. Варіанти, знову ж таки, можуть бути різними: зажадати, щоб усі нові операції були односимвольними, постулювати, що при будь-яких різночитаннях вибирається «найдовший» варіант операції (тобто доти, поки черговий набір символів, що читається транслятором, збігається з якою-небудь операцією, він продовжує зчитуватися), намагатися виявляти колізії при трансляції та видавати помилки у спірних випадках… Так чи інакше, мови, які допускають оголошення нових операцій, вирішують ці проблеми.

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

Перевантаження та поліморфні змінні

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

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

Таким чином, використання перевантаження операцій у поєднанні з поліморфними змінними робить неминучим динамічне визначення коду, що викликається.

Критика

Використання навантаження не всіма фахівцями вважається благом. Якщо навантаження функцій і процедур, загалом, не знаходить серйозних заперечень (почасти, тому, що не призводить до деяких типово «операторних» проблем, частково - через меншу спокусу її використання за призначенням), то навантаження операцій, як у принципі , і у конкретних мовних реалізаціях, піддається досить жорсткої критики із боку багатьох теоретиків і практиків програмування.

Критики відзначають, що наведені вище проблеми ідентифікації, пріоритету та асоціативності часто роблять роботу з перевантаженими операціями або невиправдано складною, або неприродною:

  • Ідентифікація. Якщо в мові прийняті жорсткі правила ідентифікації, то програміст змушений пам'ятати, для яких поєднань типів існують перевантажені операції і вручну приводити до них операнди. Якщо ж мова допускає «приблизну» ідентифікацію, ніколи не можна поручитися, що в досить складній ситуації буде виконано саме той варіант операції, який мав на увазі програміст.
  • Пріоритет та асоціативність. Якщо вони визначені жорстко - це може бути незручно і не відповідати предметній області (наприклад, для операцій із множинами пріоритети відрізняються від арифметичних). Якщо вони можуть бути задані програмістом - це стає додатковим джерелом помилок (вже хоча б тому, що різні варіанти однієї операції мають різні пріоритети, а то й асоціативність).

Наскільки зручність від користування власними операціями здатна переважити незручності від погіршення керованості програми – питання, яке не має однозначної відповіді.

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

Частина критиків висловлюються проти навантаження операцій, виходячи з загальних принципівтеорії розробки програмного забезпеченнята реальної промислової практики.

  • Прихильники «пуританського» підходу до побудови мов, такі як Вірт або Хоар, виступають проти перевантаження операцій просто тому, що без неї можна легко обійтися. На їхню думку, подібні засоби лише ускладнюють мову та транслятор, не надаючи відповідних цьому ускладненню. додаткових можливостей. На думку, сама ідея створення орієнтованого завдання розширення мови лише виглядає привабливо. Насправді використання засобів розширення мови робить програму зрозумілою тільки її автору - тому, хто це розширення розробив. Програму стає набагато важче розуміти та аналізувати іншим програмістам, що ускладнює супровід, модифікацію та групову розробку.
  • Наголошується, що сама можливість використання навантаження часто грає провокуючу роль: програмісти починають користуватися нею де тільки можливо, в результаті засіб, покликаний спростити та впорядкувати програму, стає причиною її ускладнення та заплутування.
  • Перевантажені операції можуть робити не зовсім те, що очікується від них, виходячи з їхнього виду. Наприклад, a + b зазвичай (але завжди) означає те саме, що b + a , але «один» + «два» відрізняється від «два» + «один» у мовах, де оператор + перевантажений для конкатенації рядків .
  • Перевантаження операцій робить фрагменти програми більш контекстно залежними. Не знаючи типів що беруть участь у вираженні операндов, неможливо зрозуміти, що це вираз робить, якщо у ньому використовуються перевантажені операції. Наприклад, у програмі на C++ оператор<< может означать и побитовый сдвиг, и вывод в поток. Выражение a << 1 возвращает результат побитового сдвига значения a на один бит влево, если a - целая переменная, но если a является выходным потоком , то же выражение выведет в этот поток строку «1» .

Класифікація

Нижче наведено класифікацію деяких мов програмування за тим, чи дозволяють вони перевантаження операторів, і чи обмежені оператори певним набором:

Операції Немає навантаження Є перевантаження
Обмежена безліч операцій
  • Objective-C
  • Python
Можливе визначення нових операцій
  • PostgreSQL
  • Див. також

    Wikimedia Foundation. 2010 .

    Дивитися що таке "Перевантаження функцій" в інших словниках:

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

Під навантаженням функції розуміється визначення декількох функцій (дві або більше) з однаковим ім'ям, але різними параметрами. Набори параметрів перевантажених функцій можуть відрізнятися порядком проходження, кількістю, типом. Таким чином, перевантаження функцій потрібне для того, щоб уникнути дублювання імен функцій, що виконують подібні дії, але з різною програмною логікою. Наприклад, розглянемо функцію areaRectangle() , яка обчислює площу прямокутника.

Float areaRectangle(float, float) //функція, що обчислює площу прямокутника з двома параметрами a(см) та b(см) ( return a * b; // множимо довжини сторін прямокутника і повертаємо отриманий твір )

Отже, це функція з двома параметрами типу float, причому аргументи, що передаються в функцію, повинні бути в сантиметрах, повертається значення типу float — теж у сантиметрах.

Припустимо, що наші вихідні дані (сторони прямокутника) задані в метрах та сантиметрах, наприклад: a = 2м 35 см; b = 1м 86 см. У такому разі зручно було б використовувати функцію з чотирма параметрами. Тобто, кожна довжина сторін прямокутника передається у функцію за двома параметрами: метри та сантиметри.

Float areaRectangle(float a_m, float a_sm, float b_m, float b_sm) // функція, що обчислює площу прямокутника з 4-ма параметрами a(м) a(см); b(м) b(cм) ( return (a_m * 100 + a_sm) * (b_m * 100 + b_sm); )

У тілі функції значення, які передавалися в метрах (a_m і b_m)переводяться в сантиметри і сумуються зі значеннями a_sm b_sm , після чого перемножуємо суми і отримуємо площу прямокутника см. Звичайно ж можна було перевести вихідні дані в сантиметри і користуватися першою функцією, але зараз не про це.

Тепер найголовніше – у нас є дві функції, з різною сигнатурою, але однаковими іменами (перевантажені функції). Сигнатура – ​​це комбінація імені функції із її параметрами. Як викликати ці функції? А виклик перевантажених функцій нічим не відрізняється від виклику звичайних функцій, наприклад:

AreaRectangle(32, 43); // буде викликана функція, що обчислює площу прямокутника з двома параметрами a(см) та b(см) areaRectangle(4, 43, 2, 12); // буде викликана функція, що обчислює площу прямокутника з чотирма параметрами a(м) a(см); b(м) b(cм)

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

#include "stdafx.h" #include << "S1 = " << areaRectangle(32,43) << endl; // вызов перегруженной функции 1 cout << "S2 = " << areaRectangle(4, 43, 2, 12) << endl; // вызов перегруженной функции 2 return 0; } // перегруженная функция 1 float areaRectangle(float a, float b) //функция, вычисляющая площадь прямоугольника с двумя параметрами a(см) и b(см) { return a * b; // умножаем длинны сторон прямоугольника и возвращаем полученное произведение } // перегруженная функция 2 float areaRectangle(float a_m, float a_sm, float b_m, float b_sm) // функция, вычисляющая площадь прямоугольника с 4-мя параметрами a(м) a(см); b(м) b(cм) { return (a_m * 100 + a_sm) * (b_m * 100 + b_sm); }

// код Code::Blocks

// код Dev-C++

#include using namespace std; // прототипи перевантажених функцій float areaRectangle(float a, float b); float areaRectangle(float a_m, float a_sm, float b_m, float b_sm); int main() ( cout<< "S1 = " << areaRectangle(32,43) << endl; // вызов перегруженной функции 1 cout << "S2 = " << areaRectangle(4, 43, 2, 12) << endl; // вызов перегруженной функции 2 return 0; } // перегруженная функция 1 float areaRectangle(float a, float b) //функция, вычисляющая площадь прямоугольника с двумя параметрами a(см) и b(см) { return a * b; // умножаем длинны сторон прямоугольника и возвращаем полученное произведение } // перегруженная функция 2 float areaRectangle(float a_m, float a_sm, float b_m, float b_sm) // функция, вычисляющая площадь прямоугольника с 4-мя параметрами a(м) a(см); b(м) b(cм) { return (a_m * 100 + a_sm) * (b_m * 100 + b_sm); }

Результат роботи програми показано малюнку 1.



Як добитися перевантаження функцій C? (10)

Чи є спосіб добитися перевантаження функцій C? Я дивлюся на прості функції, які можуть бути перевантажені як

foo (int a) foo (char b) foo (float c, int d)

Я думаю, що немає прямого шляху; Я шукаю обхідні шляхи, якщо такі є.

Я сподіваюся, що наведений нижче код допоможе вам зрозуміти навантаження функцій

#include #include int fun(int a, ...); int main(int argc, char *argv)( fun(1,10); fun(2,"cquestionbank"); return 0; ) int fun(int a, ...)( va_list vl; va_start(vl,a ); if(a==1) printf("%d",va_arg(vl,int)); else printf("\n%s",va_arg(vl,char *)); )

У сенсі ви маєте на увазі – ні, ви не можете.

Ви можете оголосити функцію va_arg як

void my_func(char * format, ...);

Але вам потрібно буде передати деяку інформацію про кількість змінних та їх типи в першому аргументі – наприклад, printf() .

Так начебто.

Тут ви наводите приклад:

Void printA(int a)( printf("Hello world from printA: %d\n",a); ) void printB(const char *buff)( printf("Hello world from printB: %s\n",buff) ; ) #define Max_ITEMS() 6, 5, 4, 3, 2, 1, 0 #define __VA_ARG_N(_1, _2, _3, _4, _5, _6, N, ...) N #define _Num_ARGS_(... ) __VA_ARG_N(__VA_ARGS__) #define NUM_ARGS(...) (_Num_ARGS_(_0, ## __VA_ARGS__, Max_ITEMS()) - 1) #define if(NUM_ARGS(args) #define print(x , args ...) \ CHECK_ARGS_MIN_LIMIT(1) printf("error");fflush(stdout); \ CHECK_ARGS_MAX_LIMIT(4) printf("error");fflush(stdout) ; \ (( \ if (__builtin_types_compatible_p (typeof (x), int)) \ printA(x, ##args); \ else \ printB (x,##args); \ )) int main(int argc, char* * argv) ( int a=0; print(a); print("hello"); return (EXIT_SUCCESS); )

Він буде виводити 0 і привіт.. з друкуA та друкB.

Якщо ваш компілятор - gcc, і ви не заперечуєте робити ручні оновлення щоразу, коли додаєте нове навантаження, ви можете зробити макромасу і отримати результат, який ви хочете з точки зору тих, хто дзвонить, не так приємно писати... але це можливо

подивіться на __builtin_types_compatible_p, потім використовуйте його для визначення макросу, який робить щось на зразок

#define foo(a) \ ((__builtin_types_compatible_p(int, a)?foo(a):(__builtin_types_compatible_p(float, a)?foo(a):)

але так, гидко, просто не

EDIT: C1X отримає підтримку виразів типу, які вони виглядають так:

#define cbrt(X) _Generic((X), long double: cbrtl, \ default: cbrt, \ float: cbrtf)(X)

Як було сказано, навантаження у тому сенсі, що ви маєте на увазі, не підтримується C. Звичайна ідіома для вирішення проблеми полягає в тому, що функція приймає мічений союз . Це реалізується за допомогою параметра struct де сама struct складається з деякого типу індикатора типу, такого як enum і union різних типів значень. Приклад:

#include typedef enum (T_INT, T_FLOAT, T_CHAR,) my_type; typedef struct ( my_type type; union ( int a; float b; char c; ) my_union; ) my_struct; void set_overload (my_struct *whatever) ( switch (whatever->type) ( case T_INT: whatever->my_union.a = 1; break; case T_FLOAT: whatever->my_union.b = 2.0; break; case T_CHAR: whate-> my_union.c = "3"; ) ) void printf_overload (my_struct *whatever) ( switch (whatever->type) ( case T_INT: printf("%d\n", whatever->my_union.a); break; case T_FLOAT : printf("%f\n", whatever->my_union.b); break; case T_CHAR: printf("%c\n", whatever->my_union.c); break; ) ) char* argv) ( my_struct s; s.type=T_INT; set_overload(&s); printf_overload(&s); s.type=T_FLOAT; set_overload(&s); printf_overload(&s); ; printf_overload(&s);

Чи можете ви просто використовувати C ++ і не використовувати всі інші можливості C ++, крім цього?

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

Наступний підхід схожий a2800276, але з деякими макросами макросів C99:

// we need `size_t` #include // argument types to accept enum sum_arg_types (SUM_LONG, SUM_ULONG, SUM_DOUBLE); // structure to hold an argument struct sum_arg ( enum sum_arg_types type; union ( long as_long; unsigned long as_ulong; double as_double; ) value; ); // determine an array"s size #define count(ARRAY) ((sizeof (ARRAY))/(sizeof *(ARRAY))) // this is how our function will be called #define sum(...) _sum( count(sum_args(__VA_ARGS__)), sum_args(__VA_ARGS__)) // create an array of `struct sum_arg` #define sum_args(...) ((struct sum_arg )( __VA_ARGS__ )) // create initializers for the arguments # (VALUE) ( SUM_LONG, ( .as_long = (VALUE) ) ) # define sum_ulong (VALUE) ( SUM_ULONG, ( .as_ulong = (VALUE) ) ) ) // Наші polymorphic функції long double _sum(size_t count, struct sum_arg * args) ( long double value = 0; for(size_t i = 0; i< count; ++i) { switch(args[i].type) { case SUM_LONG: value += args[i].value.as_long; break; case SUM_ULONG: value += args[i].value.as_ulong; break; case SUM_DOUBLE: value += args[i].value.as_double; break; } } return value; } // let"s see if it works #include int main() ( unsigned long foo = -1; long double value = sum(sum_long(42), sum_ulong(foo), sum_double(1e10)); printf("%Le\n", value); return 0; )

За час, _Generic моменту _Generic цього питання, стандартний C (без розширення) ефективно отримавпідтримку перевантаження функцій (а не операторів) завдяки доданню _Generic слова _Generic C11. (підтримується у GCC з версії 4.9)

(Перевантаження не є по-справжньому «вбудованим» у способі, показаному у питанні, але легко знищити те, що працює так.)

Generic - це оператор часу компіляції у тому сімействі, як і sizeof і _Alignof . Він описаний у стандартному розділі 6.5.1.1. Він приймає два основні параметри: вираз (який не оцінюватиметься під час виконання) і список асоціацій типів/виразів, який трохи схожий на блок switch. _Generic отримує загальний тип виразу, а потім "перемикається" на нього, щоб вибрати вираз кінцевого результату в списку для його типу:

Generic(1, float: 2.0, char *: "2", int: 2, default: get_two_object());

Наведене вище вираз оцінюється як 2 - тип керуючого виразу - int, тому він вибирає вираз, пов'язане з int як значення. Ніщо із цього не залишається під час виконання. (Пропозиція по default є обов'язковою: якщо ви її не вкажете, а тип не збігається, це викликає помилку компіляції.)

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

#define cbrt(X) _Generic((X), \ long double: cbrtl, \ default: cbrt, \ float: cbrtf \)(X)

Цей макрос реалізує перевантажену операцію cbrt , відправляючи на кшталт аргументу макросу, вибираючи відповідну функцію реалізації і потім передаючи вихідну макрокоманду цієї функції.

Отже, щоб реалізувати ваш оригінальний приклад, ми могли б зробити це:

Foo_int (int a) foo_char (char b) foo_float_int (float c , int d) #define foo(_1, ...) _Generic((_1), \ int: foo_int, \ char: foo_char, \ float: _Generic(( FIRST(__VA_ARGS__,)), \ int: foo_float_int))(_1, __VA_ARGS__) #define FIRST(A, ...) A

У цьому випадку ми могли б використовувати зв'язок по default: для третьої нагоди, але це не демонструє, як розширити принцип до декількох аргументів. Кінцевим результатом є те, що ви можете використовувати foo(...) у своєму коді, не турбуючись (багато) про тип своїх аргументів.

Для більш складних ситуацій, наприклад функцій, що перевантажують більшу кількість аргументів або чисел, що змінюються, ви можете використовувати макроси утиліти для автоматичного створення статичних структур відправки:

Void print_ii(int a, int b) ( printf("int, int\n"); ) void print_di(double a, int b) ( printf("double, int\n"); ) void print_iii(int a, int b, int c) ( printf("int, int, int\n"); ) void print_default(void) ( printf("unknown arguments\n"); ) #define print(...) OVERLOAD(print, (__VA_ARGS__), \(print_ii, (int, int)), \(print_di, (double, int)), \(print_iii, (int, int, int)) \) #define OVERLOAD_ARG_TYPES (int, double) #define OVERLOAD_FUNCTIONS (print) #include "activate-overloads.h" int main(void) ( print(44, 47); // prints "int, int" print(4.4, 47); // prints "double, int" print (1, 2, 3); // prints "int, int, int" print(""); // prints "unknown arguments" )

(Реалізація тут). З деякими зусиллями ви можете зменшити кількість шаблонів, щоб виглядати досить схожим на мову із вбудованою підтримкою навантаження.

Збоку вже можна було перевантажити кількістьаргументів (а чи не тип) на C99.

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

Відповідь Леушенко дійсно класна: тільки приклад foo не компілюється з GCC, який не спрацьовує при foo(7) , натрапивши на макрос FIRST і фактичний виклик функції ((_1, __VA_ARGS__) , залишаючись з додатковою комою. Крім того, у нас виникають проблеми, якщо ми хочемо забезпечити додаткові навантаження, такі як foo(double).

Тому я вирішив докладніше відповісти на це питання, у тому числі дозволити порожнє навантаження (foo(void) - що спричинило деякі неприємності...).

Ідея тепер полягає в наступному: визначте більше одного генерику в різних макросах і дозвольте вибрати правильний відповідно до кількості аргументів!

Кількість аргументів досить просто, ґрунтуючись на цій відповіді:

#define foo(...) SELECT(__VA_ARGS__)(__VA_ARGS__) #define SELECT(...) CONCAT(SELECT_, NARG(__VA_ARGS__))(__VA_ARGS__) #define CONCAT(X, Y) CONCAT_(X, Y) # define CONCAT_(X, Y) X ## Y

Це добре, ми вирішуємо або SELECT_1 або SELECT_2 (або більше аргументів, якщо ви хочете / потребуєте їх), тому нам просто потрібні відповідні визначення:

#define SELECT_0() foo_void #define SELECT_1(_1) _Generic ((_1), \ int: foo_int, \ char: foo_char, \ double: foo_double \) #define SELECT_2(_1, _2) _Generic : _Generic((_2), \ int: foo_double_int \) \)

По-перше, порожній виклик макросу (foo()) все ще створює токен, але порожній. Таким чином, лічильний макрос фактично повертає 1 замість 0 навіть при порожньому виклику макросу. Ми можемо «легко» усунути цю проблему, якщо ми __VA_ARGS__ кому після __VA_ARGS__ умовно, в залежності від того, що список порожній чи ні:

#define NARG(...) ARG4_(__VA_ARGS__ COMMA(__VA_ARGS__) 4, 3, 2, 1, 0)

Це виглядалолегко, але макрос COMMA досить тяжкий; на щастя, ця тема вже освітлена в блозі Йєнса Густедта (спасибі, Йєнс). Основний трюк полягає в тому, що макроси функцій не розширюються, якщо не слідувати дужками, для подальших пояснень дивіться блог Jens ... Нам просто потрібно трохи модифікувати макроси для наших потреб (я використовуватиму більш короткі імена і менше аргументів для стислості).

#define ARGN(...) ARGN_(__VA_ARGS__) #define ARGN_(_0, _1, _2, _3, N, ...) N #define HAS_COMMA(...) ARGN(__VA_ARGS__, 1, 1, 1, 0 ) #define SET_COMMA(...) , #define COMMA(...) SELECT_COMMA \ (\ HAS_COMMA(__VA_ARGS__), \ HAS_COMMA(__VA_ARGS__ ()), \ HAS_COMMA(SET_COMMA __VA_ARGS__), \ HAS_COMMA(SET_COMMA__) \) #define SELECT_COMMA(_0, _1, _2, _3) SELECT_COMMA_(_0, _1, _2, _3) #define SELECT_COMMA_(_0, _1, _2, _3) COMMA_ ## _0 ## _1 ## _2 # define COMMA_0000 , #define COMMA_0001 #define COMMA_0010 , // ... (all others with comma) #define COMMA_1111 ,

І тепер ми гаразд...

Повний код в одному блоці:

/* * demo.c * * Created on: 2017-09-14 * Author: sboehler */ #include void foo_void(void) ( puts("void"); ) void foo_int(int c) ( printf("int: %d\n", c); ) void foo_char(char c) ( printf("char: %c \ n ", c); int: %d\n", c, d); ) #define foo(...) SELECT(__VA_ARGS__)(__VA_ARGS__) #define SELECT(...) CONCAT(SELECT_, NARG(__VA_ARGS__))(__VA_ARGS__) # define CONCAT(X, Y) CONCAT_(X, Y) #define CONCAT_(X, Y) X ## Y #define SELECT_0() foo_void #define SELECT_1(_1) _Generic ((_1), \ int: foo_int, \ char : foo_char, \ double: foo_double \) #define SELECT_2(_1, _2) _Generic((_1), \ double: _Generic((_2), \ int: foo_double_int \) \) #define ARGN(...) ARGN_( __VA_ARGS__) #define ARGN_(_0, _1, _2, N, ...) N #define NARG(...) ARGN(__VA_ARGS__ COMMA(__VA_ARGS__) 3, 2, 1, 0) #define HAS_COMMA(...) ARGN(__VA_ARGS__, 1, 1, 0) #define SET_COMMA(...) , #define COMMA(...) SELECT_COMMA \ (\ HAS_COMMA(__VA_ARGS__), \ HAS_COMMA(__VA_ARGS__ ()), \ HAS_C OMMA(SET_COMMA __VA_ARGS__), \ HAS_COMMA(SET_COMMA __VA_ARGS__ ()) \) #define SELECT_COMMA(_0, _1, _2, _3) SELECT_COMMA_(_0, _1, _2, _3) #define SELECT _3 COMMA_ ## _0 ## _1 ## _2 ## _3 #define COMMA_0000 , #define COMMA_010 , #define COMMA_010 MA COMMA_1001 , #define COMMA_1010 , #define COMMA_1011 , #define COMMA_1100 , #define COMMA_1101 , #define COMMA_1110 , #define COMMA_1111 , int main(int arg, foo(7); foo(10.12); foo(12.10, 7); foo((char)"s"); return 0; )

Анотація: У лекції розглядаються поняття, оголошення та використання у програмах підставлюваних та перевантажених функцій у С++, механізми виконання підстановки та перевантаження функцій, рекомендації щодо підвищення ефективності програм за рахунок перевантаження або підстановки функцій.

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

Функції, що підставляються

Виклик функції , передача у ній значень, повернення значення – ці операції займають чимало процесорного часу. Зазвичай при визначенні функції компілятор резервує лише один блок осередків для збереження її операторів. Після виклику функції керування програмою передається цим операторам, а після повернення з функції виконання програми відновлюється з рядка, що настає після виклику функції.

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

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

Функції або вбудовувані (inline) функції– це функції, код яких вставляється компілятором безпосередньо на місце дзвінка замість передачі керування єдиному екземпляру функції.

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

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

Наприклад:

/*функція повертає відстань від точки з координатами(x1,y1) до точки з координатами (x2,y2)*/ inline float Line(float x1,float y1,float x2, float y2) ( return sqrt(pow(x1-x2) ,2)+pow(y1-y2,2));

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

Приклад 1.

#include "stdafx.h" #include using namespace std; inline int Cube(int x); int _tmain(int argc, _TCHAR* argv)( int x=2; float y=3; double z=4; cout<

Перерахуємо причини, з яких функція зі специфікатором inline трактуватиметься як звичайна функція, що не підставляється :

  • функція, що підставляється, є рекурсивною;
  • функції, у яких виклик розміщується до визначення;
  • функції, що викликаються більше одного разу у виразі;
  • функції, що містять цикли, перемикачі та оператори переходів;
  • функції, які мають занадто великий розмір, щоб зробити підстановку.

Обмеження виконання підстановки переважно залежить від реалізації. Якщо для функції зі специфікатором inlineкомпілятор не може виконати підстановку через контекст, в який вміщено звернення до неї, то функція вважається статичною і видається попереджувальне повідомлення.

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

Перевантаження функції

При визначенні функцій у програмах необхідно вказувати тип значення, що повертається функцією, а також кількість параметрів і тип кожного з них. Якщо мовою С++ була написана функція з ім'ям add_values, яка працювала з двома цілими значеннями, а в програмі було необхідно використовувати подібну функцію для передачі трьох цілих значень, тоді слід створити функцію з іншим ім'ям. Наприклад, add_two_values ​​та add_three_values ​​. Аналогічно, якщо необхідно використовувати подібну функцію для роботи зі значеннями типу float, то потрібна ще одна функція із ще одним ім'ям. Щоб уникнути дублювання функції, C++ дозволяє визначати кілька функцій з одним і тим самим ім'ям. У процесі компіляції C++ бере до уваги кількість аргументів, що використовуються кожною функцією, а потім викликає саме необхідну функцію. Надання компілятору вибору серед кількох функцій називається перевантаженням.

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

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

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

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

Наприклад, наступна програма перевантажує функцію з ім'ям add_values. Перше визначення функції складає два значення типу int. Друге визначення функції складає три значення типу int. У процесі компіляції C++ коректно визначає функцію, яку потрібно використовувати:

#include "stdafx.h" #include using namespace std; int add_values(int a,int b); int add_values ​​(int a, int b, int c); int _tmain(int argc, _TCHAR* argv)( cout<< "200+801=" << add_values(200,801) << "\n"; cout << "100+201+700=" << add_values(100,201,700) << "\n"; system("pause"); return 0; } int add_values(int a,int b) { return(a + b); } int add_values (int a, int b, int c) { return(a + b + c); }

Таким чином, програма визначає дві функції з іменами add_values. Перша функція становить два значення, тоді як друга складає три значення одного типу int . Компілятор мови С++ визначає, яку функцію слід використовувати, ґрунтуючись на запропонованих програмою параметрах.

Використання перевантаження функції

Одним із найбільш загальних випадків використання навантаження є застосування функції для отримання певного результату, виходячи з різних параметрів. Наприклад, припустимо, що програма має функцію з ім'ям day_of_week , яка повертає поточний день тижня (0 для неділі, 1 для понеділка, ... , 6 для суботи). Програма могла б перевантажити цю функцію таким чином, щоб вона вірно повертала день тижня, якщо їй переданий юліанський день як параметр або якщо їй передані день, місяць і рік.

int day_of_week(int julian_day) ( // оператори ) int day_of_week(int month, int day, int year) ( // оператори )

При використанні перевантажених функційчасто допускається низка помилок. Наприклад, якщо функції відрізняються тільки типом значення, що повертається, але не типами аргументів, такі функції не можуть мати однакове ім'я. Також неприпустимий наступний варіант перевантаження:

int имя_функции(int имя_аргумента); int имя_функции(int имя_аргумента); /*неприпустиме навантаження імені: аргументи мають однакову кількість та однаковий тип*/

Переваги перевантаження функції:

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

Приклад 2.

/*Перевантажені функції мають однакові імена, але різні списки параметрів і значення */ #include "stdafx.h" #include using namespace std; int average(int first_number, int second_number, int third_number); int average(int first_number, int second_number); int _tmain(int argc, _TCHAR* argv)(// головна функція int number_A = 5, number_B = 3, number_C = 10; cout<< "Целочисленное среднее чисел " << number_A << " и "; cout << number_B << " равно "; cout << average(number_A, number_B) << ".\n\n"; cout << "Целочисленное среднее чисел " << number_A << ", "; cout << number_B << " и " << number_C << " равно "; cout << average(number_A, number_B, number_C) << ".\n"; system("PAUSE"); return 0; }// конец главной функции /*функция для вычисления целочисленного среднего значения 3-х целых чисел*/ int average(int first_number, int second_number, int third_number) { return((first_number + second_number + third_number)/3); } // конец функции /*функция для вычисления целочисленного среднего значения 2-х целых чисел*/ int average(int first_number, int second_number) { return((first_number + second_number)/2); } // конец функции



Завантаження...
Top