Урок №111. Эллипсис

  Юрий  | 

    | 

  Обновл. 9 Июн 2019  | 

 6944

Во всех функциях, которые мы рассматривали до сих пор, количество их параметров должно было быть известно заранее (даже если это параметры по умолчанию). Однако есть несколько случаев, в которых полезно передать переменную, указывающую на количество параметров, в функцию. В 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

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

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

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

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

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