Урок 111. Эллипсис. Почему его не следует использовать

  Юрий Ворон  | 

    | 

  Обновлено 30 Янв 2018  | 

 3172

Во всех функциях, которые мы рассматривали до сих пор, количество параметров этих функций должно было быть известно заранее (даже если это параметры по умолчанию). Однако есть несколько случаев, в которых полезно передать переменную, указывающую на количество параметров, в функцию. В C++ есть специальный объект, который позволяет это делать — эллипсис.

Поскольку эллипсис редко используется и считается потенциально опасным, то мы рекомендуем избегать его использования вообще. Этот урок не является обязательным в продвижении изучения С++ и на ваш прогресс не повлияет, если это вам интересно – продолжайте читать, если нет, то нет.

Функции, использующие эллипсис, выглядят следующим образом:

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

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

Эллипсис (который представлен в виде многоточия … ) всегда должен быть последним параметром в функции. О нём можно думать, как о массиве, который содержит любые другие параметры, кроме тех, что указаны в списке_аргументов.

Пример эллипсиса

Лучший способ разобраться с эллипсисом – рассмотреть пример с ним. Предположим, мы хотим написать функцию, которая вычисляет среднее арифметическое переданных аргументов:

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

2.5
3

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



Во-первых, мы должны подключить заголовочный файл cstdarg. Этот заголовок определяет va_list, va_start и va_end — макросы, которые нам нужно использовать для доступа к параметрам, которые являются частью эллипсиса.

Затем мы объявляем функцию, которая использует эллипсис. Помните, что список_аргументов должен быть представлен одним или несколькими фиксированными параметрами. Здесь мы передаем одно целочисленное значение, которое сообщает функции, сколько будет параметров.

Обратите внимание, в эллипсисе нет никаких имён переменных! Вместо этого мы получаем доступ к значениям через специальный тип — va_list. О va_list можно думать, как об указателе, который указывает на массив с эллипсисом. Сначала мы объявляем переменную va_list, которую называем просто «list» для удобства использования.

Затем нам нужно чтобы list указывал на параметры эллипсиса. Делается это с помощью va_start(). va_start() имеет два параметра: va_list и имя последнего параметра не-эллипсиса функции. После того, как va_start() был вызван, va_list указывает на первый параметр в многоточии.

Чтобы получить значение параметра, на который указывает va_list – нужно использовать va_arg(). 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

Этот результат подтверждает фразу: «Мусор на входе, мусор на выходе».

Почему эллипсис небезопасен: Эллипсис не знает количество переданных параметров

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

Способ №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 (19 оценок, среднее: 4,95 из 5)
Загрузка...

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

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

ВОЛШЕБНАЯ ТАБЛЕТКА ПО С++