Переопределение функции c. Перегрузка функций. Преимущества перегрузки функции

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

Цель лекции : изучить подставляемые (встраиваемые) функции и перегрузки функций, научиться разрабатывать программы с использованием перегрузки функций на языке 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); } // конец функции



Как добиться перегрузки функций в 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 CHECK_ARGS_MAX_LIMIT(t) if(NUM_ARGS(args)>t) #define CHECK_ARGS_MIN_LIMIT(t) 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: whatever->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; } } int main (int argc, 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); s.type=T_CHAR; set_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 }; // a 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 #define sum_long(VALUE) { SUM_LONG, { .as_long = (VALUE) } } #define sum_ulong(VALUE) { SUM_ULONG, { .as_ulong = (VALUE) } } #define sum_double(VALUE) { SUM_DOUBLE, { .as_double = (VALUE) } } // our polymorphic function 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((_1), \ double: _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 __VA_ARGS__ ()) \) #define SELECT_COMMA(_0, _1, _2, _3) SELECT_COMMA_(_0, _1, _2, _3) #define SELECT_COMMA_(_0, _1, _2, _3) COMMA_ ## _0 ## _1 ## _2 ## _3 #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); } void foo_double(double c) { printf("double: %.2f\n", c); } void foo_double_int(double c, int d) { printf("double: %.2f, 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_COMMA(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_COMMA_(_0, _1, _2, _3) COMMA_ ## _0 ## _1 ## _2 ## _3 #define COMMA_0000 , #define COMMA_0001 #define COMMA_0010 , #define COMMA_0011 , #define COMMA_0100 , #define COMMA_0101 , #define COMMA_0110 , #define COMMA_0111 , #define COMMA_1000 , #define COMMA_1001 , #define COMMA_1010 , #define COMMA_1011 , #define COMMA_1100 , #define COMMA_1101 , #define COMMA_1110 , #define COMMA_1111 , int main(int argc, char** argv) { foo(); foo(7); foo(10.12); foo(12.10, 7); foo((char)"s"); return 0; }

C ++ позволяет указать более одного определения для функции имени или оператора в той же области, что называется функция перегрузки и перегрузки операторов соответственно.

Перегруженное объявление представляет собой объявление, объявленное с тем же именем, что и ранее объявленное объявление в той же области видимости, за исключением того, что обе декларации имеют разные аргументы и, очевидно, другое определение (реализация).

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

Перегрузка функций в C ++

Вы можете иметь несколько определений для одного и того же имени функции в той же области. Определение функции должно отличаться друг от друга по типам и / или количеству аргументов в списке аргументов. Вы не можете перегружать объявления функций, которые отличаются только возвращаемым типом.

Ниже приведен пример, когда одна и та же функция print () используется для печати разных типов данных -

#include using namespace std; class printData { public: void print(int i) { cout << "Printing int: " << i << endl; } void print(double f) { cout << "Printing float: " << f << endl; } void print(char* c) { cout << "Printing character: " << c << endl; } }; int main(void) { printData pd; // Call print to print integer pd.print(5); // Call print to print float pd.print(500.263); // Call print to print character pd.print("Hello C++"); return 0; }

Printing int: 5 Printing float: 500.263 Printing character: Hello C++

Перегрузка операторов в C ++

Вы можете переопределить или перегрузить большинство встроенных операторов, доступных на C ++. Таким образом, программист может также использовать операторы с определенными пользователем типами.

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

Box operator+(const Box&);

объявляет оператор добавления, который можно использовать для добавления двух объектов Box и возвращает конечный объект Box. Большинство перегруженных операторов могут быть определены как обычные функции, не являющиеся членами, или как функции членов класса. В случае, если мы определяем функцию выше как функцию, не являющуюся членом класса, мы должны были бы передать два аргумента для каждого операнда следующим образом:

Box operator+(const Box&, const Box&);

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

#include using namespace std; class Box { public: double getVolume(void) { return length * breadth * height; } void setLength(double len) { length = len; } void setBreadth(double bre) { breadth = bre; } void setHeight(double hei) { height = hei; } // Overload + operator to add two Box objects. Box operator+(const Box& b) { Box box; box.length = this->length + b.length; box.breadth = this->breadth + b.breadth; box.height = this->height + b.height; return box; } private: double length; // Length of a box double breadth; // Breadth of a box double height; // Height of a box }; // Main function for the program int main() { Box Box1; // Declare Box1 of type Box Box Box2; // Declare Box2 of type Box Box Box3; // Declare Box3 of type Box double volume = 0.0; // Store the volume of a box here // box 1 specification Box1.setLength(6.0); Box1.setBreadth(7.0); Box1.setHeight(5.0); // box 2 specification Box2.setLength(12.0); Box2.setBreadth(13.0); Box2.setHeight(10.0); // volume of box 1 volume = Box1.getVolume(); cout << "Volume of Box1: " << volume <

Когда приведенный выше код компилируется и выполняется, он производит следующий результат:

Volume of Box1: 210 Volume of Box2: 1560 Volume of Box3: 5400

Перегружаемый / Non-overloadableOperators

Ниже приведен список операторов, которые могут быть перегружены.

Под перегрузкой функции понимается, определение нескольких функций (две или больше) с одинаковым именем, но различными параметрами. Наборы параметров перегруженных функций могут отличаться порядком следования, количеством, типом. Таким образом перегрузка функций нужна для того, чтобы избежать дублирования имён функций, выполняющих сходные действия, но с различной программной логикой. Например, рассмотрим функцию 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); // будет вызвана функция, вычисляющая площадь прямоугольника с 4-мя параметрами 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.

Перегрузка функций

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

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

Термин «перегрузка» - это калька английского «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 .

    Смотреть что такое "Перегрузка функций" в других словарях:

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