Урок №117. Еліпсис

  Юрій  | 

  Оновл. 24 Січ 2021  | 

 61

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

Еліпсис

Функції, які використовують еліпсис, виглядають наступним чином:

тип_повернення ім'я_функції(список_аргументів, ...)

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

Еліпсис (англ. “ellipsis”), який представлений у вигляді трьох крапок в мові C++, завжди повинен бути останнім параметром у функції. Про нього можна думати, як про масив, який містить будь-які інші параметри, крім тих, які вказані в список_аргументів.

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

Результат виконання програми:

2.5
3

Як ви можете бачити, функція findAverage() приймає змінну count, яка вказує на кількість переданих аргументів. Розглянемо інші компоненти цього прикладу.

По-перше, ми повинні підключити заголовок cstdarg. Цей заголовок визначає va_list, va_start і va_end — макроси, необхідні для доступу до параметрів, які є частиною еліпсиса.

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

Зверніть увагу, в еліпсисі немає ніяких імен змінних! Замість цього ми отримуємо доступ до значень через спеціальний тип — va_list. Про va_list можна думати, як про вказівник, який вказує на масив з еліпсисом. Спочатку ми оголошуємо змінну va_list, яку називаємо просто list для зручності використання.

Потім нам потрібно, щоб list вказував на параметри еліпсиса. Робиться це за допомогою va_start(), який має два параметри: va_list і ім’я останнього параметра, який не є еліпсисом. Після того, як va_start() був викликаний, va_list вказує на перший параметр зі списку переданих аргументів.

Щоб отримати значення параметра, на який вказує va_list, потрібно використати va_arg(), який також має два параметри: va_list і тип даних параметра, до якого ми намагаємося отримати доступ. Зверніть увагу, за допомогою va_arg() ми також переходимо до наступного параметру va_list!

Нарешті, коли ми вже все зробили, потрібно виконати очистку: va_end() з параметром va_list.

Чому еліпсис небезпечний?

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

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

Хоча на перший погляд все може здатися досить звичайним, але подивіться на другий аргумент типу double — він повинен бути типу int. Хоча все скомпілюється без помилок, але результат наступний:

1.78782e+08

Число не маленьке. Як це сталося?

Як ми вже знаємо з попередніх уроків, комп’ютер зберігає всі дані у вигляді послідовності біт. Тип змінної вказує комп’ютеру, як перевести цю послідовність біт в певне (читабельне) значення. Однак в еліпсисі тип змінної відкидається. Отже, єдиний спосіб отримати нормальне значення назад з еліпсису — вручну вказати va_arg(), який очікуваний тип параметра. Це те, що робить другий параметр в va_arg(). Якщо фактичний тип параметра не відповідає очікуваному типу параметра, то відбудуться погані речі.

У програмі, наведеній вище, за допомогою va_arg(), ми вказали, що всі параметри повинні бути типу int. Отже, кожен виклик va_arg() буде повертати послідовність біт, яка буде конвертована в тип int.

У цьому випадку проблема полягає в тому, що значення типу double, яке ми передали в якості першого аргументу еліпсиса, займає 8 байт, тоді як va_arg(list, int) повертає тільки 4 байта даних при кожному виклику (тип int займає 4 байти). Отже, перший виклик va_arg повертає першу частину типу double (4 байти), а другий виклик va_arg повертає другу частину типу double (ще 4 байти). В результаті отримуємо сміття.

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

Вірте чи ні, але це дійсно скомпілювалося без помилок і видало наступний результат на моєму комп’ютері:

1.56805e+08

Цей результат підтверджує фразу: «Сміття на вході, сміття на виході».

Мало того, що еліпсис відкидає тип параметрів, він також відкидає і кількість цих параметрів. Це означає, що нам потрібно буде самим розробити рішення для відстеження кількості параметрів, що передаються в еліпсис. Як правило, це робиться одним з наступних 3-х способів:

Спосіб №1: Передати параметр-довжину. Потрібно, щоб один з фіксованих параметрів, який не входить в еліпсис, відображав кількість переданих параметрів. Це рішення використовувалося у вищенаведеній програмі. Однак навіть тут ми зіткнемося з проблемами, наприклад:

Результат на моєму комп’ютері:

4.16667

Що трапилося? Ми повідомили findAverage(), що збираємося передати 6 значень, але фактично передали тільки 5. Отже, з першими п’ятьма значеннями, що повертаються, функції va_arg() все ок. Але ось 6-е значення, яке повертає va_arg() — це просто сміття зі стеку, так як ми його не передавали. Отже, і результат відповідний. Принаймні, тут очевидно, що це значення є сміттям. А ось розглянемо більш підступний випадок:

Результат:

3.5

На перший погляд все коректно, але останнє число (7) в списку аргументів ігнорується, так як ми повідомили, що збираємося надати 6 параметрів (а надали 7). Такі помилки буває досить-таки важко виявити.

Спосіб №2: Використовувати контрольне значення. Контрольне значення — це спеціальне значення, яке при його виявленні використовується для завершення циклу. Наприклад, нуль-термінатор використовується в рядках для позначення кінця рядка. У еліпсиса контрольне значення передається останнім з аргументів. Ось програма, наведена вище, але вже з використанням контрольного значення -1:

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

Однак тут також є нюанси. По-перше, мова C++ вимагає, щоб ми передавали хоча б один фіксований параметр. У попередньому прикладі для цього використовувалася змінна count. У цьому прикладі перше значення є частиною чисел, використовуваних в обчисленні. Тому, замість обробки першого значення в парі з іншими параметрами еліпсиса, ми явно оголошуємо його як звичайний параметр. Потім нам потрібно це обробити усередині функції (ми присвоюємо змінній sum значення first, а не 0, як у попередній програмі).

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

Спосіб №3: Використовувати рядок-декодер. Передайте рядок-декодер в функцію, щоб повідомити, як правильно інтерпретувати параметри:

У цьому прикладі ми передаємо рядок, в якому вказується як кількість переданих аргументів, так і їх типи (i = int, d = double). Таким чином, ми можемо працювати з параметрами різних типів. Однак слід пам’ятати, що якщо число або типи переданих параметрів не будуть з точністю відповідати тому, що зазначено в рядку-декодері, то можуть відбутися погані речі.

Рекомендації по безпечному використанню еліпсиса

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

По-друге, якщо ви використовуєте еліпсис, не змішуйте різні типи аргументів в межах вашого еліпсиса, якщо це можливо. Це зменшить ймовірність того, що caller випадково передасть дані не того типу, а va_arg() видасть результат-сміття.

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

Оцінити статтю:

1 Зірка2 Зірки3 Зірки4 Зірки5 Зірок (1 оцінок, середня: 5,00 з 5)
Loading...

Залишити відповідь

Ваш E-mail не буде опублікований. Обов'язкові поля відмічені *