search

Функции с переменным числом параметров

Язык C++ вслед за С позволяет писать функции с переменным числом параметров. Одним из простых примеров может служить функция, вычисляющая среднее арифметическое своих аргументов. Другой уже классический пример — функция сцепления произвольного количества строк, которая является естественным обобщением функции сцепления двух строк.
Переменный список параметров задается в заголовке функции многоточием:

int f(…)

Этот заголовок не вызывает у компилятора протестов. Такая запись означает, что при определении функции компилятору неизвестны ни количество параметров, ни их типы, и он, естественно, не может ничего проверить. Количество параметров и их типы становятся известными только при вызове функции. Однако у программиста с написанием таких функций сразу возникают проблемы. Ведь имена параметров отсутствуют! Поэтому доступ можно осуществить только одним способом – косвенным, используя указатель. Вспомним, что все параметры при вызове помещаются в стек. Если мы каким-то образом установим указатель на начало списка параметров в стеке, то, манипулируя с указателем, мы, в принципе, можем «достать» все параметры! Таким образом, список параметров совсем пустой быть не может, должен быть прописан хотя бы один явный параметр, адрес которого мы можем получить при выполнении программы. Заголовок такой функции может выглядеть так:

int f(int k...)

Ни запятая, ни пробел после параметра не обязательны, хотя можно их и прописать. Есть одно обстоятельство, которое ограничивает применение таких функций: при написании функции с переменным числом параметров помимо алгоритма обработки программист должен разрабатывать и алгоритм доступа к параметрам. Так что список необъявленных параметров не может быть совсем уж произвольным – в языке C++ не существует универсальных средств распознавания элементов этого списка. Это же означает, что передача аргумента не того типа, который задумывался, или не тем способом, который подразумевался при разработке, приведет к катастрофическим последствиям – компилятор-то ничего не проверяет. Попробуем написать функцию, вычисляющую среднее арифметическое своих аргументов. Для этого требуется решить несколько проблем как установиться на список параметров в стеке;
как «перебирать» параметры; как закончить перебор.

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

И тот, и другой способ имеют право на жизнь — все определяется потребностями задачи и вкусами программиста. В данном случае сначала попробуем второй способ: последним значением списка параметров будет ноль


double f(double n, ...)    //--заголовок с переменным числом параметров
{   double *p = &n;        //--установились на начало списка параметров
    double sum = 0, count = 0;    
    while (*p)         //--пока аргумент не равен нулю
    { sum+=(*p);         //--суммируем аргумент
      p++;             //--«перемещаемся на следующий аргумент
      count++;         //--считаем  количество аргументов
    }
    return ((sum)?sum/count:0);    //--вычисляем среднее
}

Вызов такой функции может выглядеть таким образом:
double y = f(1.0, 2.0, 3.0, 4.0, 0.0);
Переменная y получит значение 2.5. Так как компилятор ничего не проверяет, то попытка вызвать такую функцию с целыми аргументами f(1,2,3,0) либо вызовет аварийный останов программы (это лучший вариант), либо в приводит к неверному (но правдоподобному — в этом главная опасность) результату.
Реализация функции, которая в качестве первого параметра получает количество аргументов, на первый взгляд, не вызывает затруднений. Однако, если первый аргумент – целое число, то требуется преобразование указателя. И тут не все варианты проходят. Не будет работать такой вариант:


double f(int n, ...)            //--количество элементов
{   int *p = &n;            //--указатель – «целый»
    double sum = 0, count = n;
    for (;n--;(double*)p++)         //--преобразование int* ->double* 
      sum+=(*p); 
    return ((sum)?sum/count:0);
}
//Такой вариант тоже неработоспособен
double f(int n, ...)            //--количество элементов
{   double *p = (double *)&n;    //--преобразование адреса
    double sum = 0, count = n;
    for (;n--;p++)             //--изменение указателя 
      sum+=(*p); 
    return ((sum)?sum/count:0); 
}

Причина кроется в том, что изменение указателя производится на столько байт, сколько в памяти занимает базовый тип. В обоих случаях мы установились не на начало списка double-параметров, а на sizeof(int) байтов «раньше» — на целую переменную. И от этого адреса происходит изменение указателя на 8 байт (sizeof(double)), что приводит к совершенно неверным результатам. Решение заключается в том, чтобы сначала изменить «целый» указатель, а потом уже его преобразовать в double *. Так всегда необходимо делать, если тип первого параметра отличается от типов отсутствующих параметров.
Листинг 7.8. Вычисление среднего арифметического аргументов (количество)


double f(int n, ...)            //--количество элементов
{  int *p = &n;
    p++;                //-3-установка «целого» на double
    double *pp = (double *)p;    //--преобразование типа указателя
    double sum = 0, count = n;
    for (;n--;pp++)             //--правильное увеличение на 8
       sum+=(*pp); 
    return ((sum)?sum/count:0);
}

В строке //-3- операция p++ устанавливает указатель на первый элемент списка параметров типа double. Для дальнейшего изменения указателя на 8 мы использовали преобразование типа указателя:
double *pp = (double *)p;
После этой строки операция pp++ будет увеличивать указатель на sizeof(double)=8, что нам и требуется.
Мы использовали способ передачи параметров по значению. В этом случае в качестве фактических аргументов можно задавать произвольные выражения. Однако можно использовать и передачу ссылки – это несколько осложняет вызов, поскольку в этом случае в списке аргументов могут прописываться только переменные. Необходимо также помнить, что все фактические аргументы должны передаваться одинаковым способом. Тогда прототип первого варианта функции выглядит так (тело функции не изменяется):
double f(double &n, ...)
При вызове можно использовать элементы массива:
double m[] = {1.0,2.0,3.0,4.0,0.0};
cout << f(m[0],m[1],m[2],m[3],m[4]) << endl;
Эта программа выведет на экран 2.5.
Язык C++ в качестве элементов переменного списка аргументов разрешает прописывать указатели. Однако обработка такого варианта вызывает сложности – нам требуется двойной косвенный доступ, а указатель для доступа к стеку — «одноразовый». Таким образом, если передавать параметры-указатели, то в приведенной выше программе (с первым параметром-количеством) значение *рр – это не число типа double, а адрес этого числа. Тут без «обмана» компилятора не обойтись, поскольку он просто так не пропускает преобразование «одноразовой» косвенности в двойную. Но мы помним, что все типизированные указатели, независимо от типа и косвенности всегда представляют собой адрес, размер которого в Intel – 4 байта. Тогда можно использовать union


double f(int n, ...)
{  int *p = &n;        //--«одноразовый» указатель    
    p++;                    //--«достаем» список параметровd-указателей
   union Pointer 
   {double **pp; double *kp; };    //--«подстава» указателей
    Pointer A;
    A.kp = (double *)p;           //-«обманываем» компилятора
    double sum = 0, count = n;
    for (;n--;A.pp++)         //--изменяем двойной указатель!
         sum+=(**A.pp);         //--двойной доступ!
    return ((sum)?sum/count:0);
}

Хотя по стандарту такое использование union означает undefined behaviour (неопределенное поведение), и непереносимо, но на практике (например, на платформе Intel) работает хорошо. Однако при переносе на другую платформу надо будет проверять корректность работы такой функции.
Необходимо обратить внимание на то, что изменяем мы «двойной» указатель – «одноразовый» использовать нельзя, поскольку будет изменение на sizeof(double)=8 байт, а нам требуется изменение на sizeof(double*)= 4 байта. И при суммировании используется двойной косвенный доступ. Обращение к такой функции выполняется так:
cout << f(2,&a,&b) << endl;
Аналогичная функция без счетчика аргументов может использовать в качестве признака окончания списка параметров нулевой указатель. Но в данном случае -
«обманывать» компилятор не требуется — в функции непосредственно используется «двойной» указатель.
Переменный список параметров-указателей (ноль в конце)



double f(double *a, ...)
{   double **p = &a;         //--берем адрес-адреса
    double sum = 0, count = 0;
    while (*p!=0)         //-- NULL – прямо в списке параметров
    { sum+=(**p);         //--выбираем значения
       count++; p++;        //--бежим по списку
    }
    return ((sum)?sum/count:0);
}

Вызов такой функции выглядит так:
f(&a,&b,0) Особо обратите внимание на следующее: в списке параметров 0 – это значение, а не адрес. Поэтому в теле функции проверка окончания цикла делается с одной звездочкой, а не с двумя.
Напоследок осталось рассмотреть пример, в котором список указателей переменной длины составляют указатели на char. Мы выделяем этот случай по двум причинам: размер данных (char) меньше, чем размер указателя (char*) – в остальных случаях размер данных больше или равен размеру указателя;
char* — это единственный указатель, вместо которого при вызове можно задавать не адрес.
Типичной функцией, в которой можно применить переменный список параметров, является функция сцепления произвольного количества строк в одну. Заголовок такой функции может выглядеть так:
char *f(char *s1, ...)
Функция должна сначала вычислить количество памяти, необходимой для целевой строки, а потом уже помещать туда результат сцепления. Используем тот же прием,
что и в предыдущем примере – последний параметр должен быть 0. Сцепление строк (ноль в конце)


char *f(char *s1, ...)
{ char **cp = &s1;            //--адрес первого указателя
   int len = 0;        
   // цикл для определения общей длины сцепляемых строк
   while (*cp) { len += strlen(*cp); cp++; }                          
  char *s = new char[len+1];       //--память для строки
  s[0]=0;                       //-- "очищаем" строку
// цикл для сцепления строк
  cp=&s1;                       //-- опять установка на 1-й параметр
  while (*cp) 
  {  strcat(s, *cp);             //-- прицепляем первую (и следующие)
     cp++;                       //-- перемещаемся на следующую
  }         
  return s;
}

Вызов такой функции может быть таким
char *ss = f(s1, s2, s3, 0);
где s1, s2, s3 – это либо объявленные константы, либо переменные типа char *. Ту же функцию можно вызывать и с явно прописанными константами:
char *sd = f(“First “,“Two “,“Three “, 0);
Очевидно, вместо параметра-указателя (ссылки) можно подставлять выражение, имеющее результатом указатель (ссылку). В частности, на месте указателя на char можно вызвать функцию, которая вводит строку с клавиатуры.
Стандартные средства
В стандарт языка входит набор макросов для работы со списками параметров переменной длины, определенный в stdarg.h. При их использовании точно так же требуется указывать в списке явный параметр, объявить и установить на него указатель и перемещаться по списку, изменяя его. В конце списка должен стоять NULL. Макросы, обеспечивающие стандартный доступ к спискам параметров переменной длины, имеют следующий формат:
void va_start(va_list prm, последний-явный-параметр);
тип va_arg(va_list prm, тип);
void va_end(va_list prm);
Тип указателя определяется с помощью оператора typedef как va_list. Макрос va_start устанавливает указатель типа va_list на явный параметр, макрос va_arg перемещает указатель на следующий параметр, а макрос va_end обнуляет указатель. Указанные макросы используются следующим образом: 1. в теле функции с переменным числом параметров до первого использования указанных макросов должно появиться объявление объекта типа va_list, например va_list LastP; фактически это является объявлением указателя;
2. указанный объект связывается с последним явным параметром (перед многоточием) переменного списка параметров с помощью макроса va_start, например va_start(LastP,P); таким образом происходит инициализация указателя;
3. передвижение по переменному списку параметров выполняется макросом va_arg. Для этого, как указано выше, тоже необходимо явно указывать тип очередного параметра, то есть программист должен его знать в момент написания программы. Если все параметры в списке целого типа, то вызов va_arg выглядит так: va_arg(LastP,int);
4. после всей обработки ставится вызов va_end, например va_end(LastP).
В качестве примера рассмотрим реализацию функции вычисления среднего арифметического (вариант с количеством аргументов) с использованием этих макросов
Вычисление среднего с использование стандартных средств (количество)


double f(int n, double a, ...)
{   va_list p;             //--объявление указателя
    double sum = 0, count = 0;
    va_start(p, n);            //--инициализация указателя
    while(n--)             
    { sum+=va_arg(p,double);        //--перемещение указателя 
      count++; 
    }
    va_end(p);                //--«закрытие» указателя
    return ((sum)?sum/count:0);
}

Очень похоже выглядит и вариант с нулем в конце списка
Вычисление среднего стандартными средствами (ноль в конце)


double f(double a, ...)
{   va_list p;             //--объявление указателя
    double sum = 0, count = 0;
    va_start(p, a);            //--инициализация указателя
    double k = a;            //--промежуточная переменная 
    do { sum+=k;  count++;
    } while(k=va_arg(p,double));     //--пока не ноль -передвигаемся
    va_end(p);                //--«закрыли» указатель
    return ((sum)? sum/count: 0);
}

Однако в этом случае удобно использовать цикл do while, так как указатель p сразу устанавливается на слагаемое. Передвижение по списку выполняется прямо в условии цикла, что обеспечивает одновременную проверку на ноль.