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

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

Например, рассмотрим print функцию, принимающую std::string аргумент. Эта функция может выполнять очень разные задачи, отличные от функции, которая принимает аргумент типа double. Перегрузка позволяет использовать такие имена, как print_string или print_double. Во время компиляции компилятор выбирает перегрузку, используемую на основе типов и количества аргументов, переданных вызывающим оператором. При вызове print(42.0)void print(double d) вызывается функция. При вызове print("hello world")вызывается перегрузка void print(std::string) .

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

Заметки по перегрузке

Элемент объявления функции Используется для перегрузки?
Тип возвращаемого функцией значения No
Число аргументов Да
Тип аргументов Да
Наличие или отсутствие многоточия Да
typedef Использование имен No
Незаданные границы массива No
const или volatile Да, при применении ко всей функции
Ссылочные квалификаторы (& и &&) Да

Пример

В следующем примере показано, как использовать перегрузки функций:

// function_overloading.cpp
// compile with: /EHsc
#include <iostream>
#include <math.h>
#include <string>

// Prototype three print functions.
int print(std::string s);             // Print a string.
int print(double dvalue);            // Print a double.
int print(double dvalue, int prec);  // Print a double with a
                                     //  given precision.
using namespace std;
int main(int argc, char *argv[])
{
    const double d = 893094.2987;
    if (argc < 2)
    {
        // These calls to print invoke print( char *s ).
        print("This program requires one argument.");
        print("The argument specifies the number of");
        print("digits precision for the second number");
        print("printed.");
        exit(0);
    }

    // Invoke print( double dvalue ).
    print(d);

    // Invoke print( double dvalue, int prec ).
    print(d, atoi(argv[1]));
}

// Print a string.
int print(string s)
{
    cout << s << endl;
    return cout.good();
}

// Print a double in default precision.
int print(double dvalue)
{
    cout << dvalue << endl;
    return cout.good();
}

//  Print a double in specified precision.
//  Positive numbers for precision indicate how many digits
//  precision after the decimal point to show. Negative
//  numbers for precision indicate where to round the number
//  to the left of the decimal point.
int print(double dvalue, int prec)
{
    // Use table-lookup for rounding/truncation.
    static const double rgPow10[] = {
        10E-7, 10E-6, 10E-5, 10E-4, 10E-3, 10E-2, 10E-1,
        10E0, 10E1,  10E2,  10E3,  10E4, 10E5,  10E6 };
    const int iPowZero = 6;

    // If precision out of range, just print the number.
    if (prec < -6 || prec > 7)
    {
        return print(dvalue);
    }
    // Scale, truncate, then rescale.
    dvalue = floor(dvalue / rgPow10[iPowZero - prec]) *
        rgPow10[iPowZero - prec];
    cout << dvalue << endl;
    return cout.good();
}

В приведенном выше коде показаны перегрузки print функции в область файла.

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

Аргументы по умолчанию не могут быть предоставлены для перегруженных операторов.

Сопоставление аргументов

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

  • Точное соответствие найдено.

  • Тривиальное преобразование выполнено.

  • Восходящее приведение целого типа выполнено.

  • Стандартное преобразование в требуемый тип аргумента существует.

  • Определяемое пользователем преобразование (оператор преобразования или конструктор) в нужный тип аргумента существует.

  • Аргументы, представленные многоточием, найдены.

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

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

Рассмотрим следующие объявления (функции отмечены как Variant 1, Variant 2 и Variant 3 для ссылки в последующем обсуждении).

Fraction &Add( Fraction &f, long l );       // Variant 1
Fraction &Add( long l, Fraction &f );       // Variant 2
Fraction &Add( Fraction &f, Fraction &f );  // Variant 3

Fraction F1, F2;

Рассмотрим следующий оператор.

F1 = Add( F2, 23 );

Представленный выше оператор создает два набора.

Set 1: Кандидат функции, имеющие первый аргумент типа Fraction Set 2: Кандидат функций, второй аргумент которого можно преобразовать в тип int
Variant 1 Вариант 1 (int можно преобразовать в long использование стандартного преобразования)
Variant 3

Функции в Set 2 — это функции, которые имеют неявные преобразования из фактического типа параметра в формальный тип параметра. Одна из этих функций имеет наименьшую "стоимость" для преобразования фактического типа параметра в соответствующий формальный тип параметра.

Пересечением этих двух наборов является функция Variant 1. Ниже представлен пример неоднозначного вызова функции.

F1 = Add( 3, 6 );

В предыдущем вызове функции создаются следующие наборы.

Задать 1. Кандидаты функции с первым аргументом типа int Задать 2. Кандидаты функции, имеющие второй аргумент типа int
Вариант 2 (int можно преобразовать в long использование стандартного преобразования) Вариант 1 (int можно преобразовать в long использование стандартного преобразования)

Так как пересечение этих двух наборов пусто, компилятор создает сообщение об ошибке.

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

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

Примечание.

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

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

Перегруженные функции различают типы аргументов, имеющие разные инициализаторы. Следовательно, аргумент заданного типа и ссылка на этот тип считаются одинаковыми для перегрузки, Они считаются одинаковыми, так как они принимают те же инициализаторы. Например, max( double, double ) — то же самое, что и max( double &, double & ). Объявление двух таких функций приводит к ошибке.

По той же причине аргументы функций типа, измененные const или volatile не обрабатываются иначе, чем базовый тип в целях перегрузки.

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

// argument_type_differences.cpp
// compile with: /EHsc /W3
// C4521 expected
#include <iostream>

using namespace std;
class Over {
public:
   Over() { cout << "Over default constructor\n"; }
   Over( Over &o ) { cout << "Over&\n"; }
   Over( const Over &co ) { cout << "const Over&\n"; }
   Over( volatile Over &vo ) { cout << "volatile Over&\n"; }
};

int main() {
   Over o1;            // Calls default constructor.
   Over o2( o1 );      // Calls Over( Over& ).
   const Over o3;      // Calls default constructor.
   Over o4( o3 );      // Calls Over( const Over& ).
   volatile Over o5;   // Calls default constructor.
   Over o6( o5 );      // Calls Over( volatile Over& ).
}

Выходные данные

Over default constructor
Over&
Over default constructor
const Over&
Over default constructor
volatile Over&

Указатели на constvolatile объекты также считаются разными от указателей к базовому типу в целях перегрузки.

Сопоставление аргументов и преобразования

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

  • Последовательности преобразований, содержащих несколько определяемых пользователем преобразований, не рассматриваются.

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

Результирующая последовательность преобразований, если она есть, называется лучшей последовательностью сопоставления. Существует несколько способов преобразования объекта типа в тип intunsigned long с помощью стандартных преобразований (описанных в стандартных преобразованиях):

  • Преобразуйте из intlong и unsigned longзатем из long .

  • Преобразование из intunsigned long.

Хотя первая последовательность достигает желаемой цели, она не является лучшей последовательностью сопоставления, так как короткая последовательность существует.

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

Тривиальные преобразования

Тип аргумента Преобразованный тип
type-name type-name&
type-name& type-name
type-name[] type-name*
type-name(argument-list) (*type-name)(argument-list)
type-name const type-name
type-name volatile type-name
type-name* const type-name*
type-name* volatile type-name*

Ниже приведена последовательность, в которой делаются попытки выполнения преобразований.

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

    • От указателя до указателя на const (type-name* на const type-name*).

    • От указателя до указателя на volatile (type-name* на volatile type-name*).

    • Ссылка на const (type-name& to const type-name&).

    • Ссылка на volatile (type-name& to volatile type&).

  2. Сопоставление с использованием повышений. Любая последовательность, которая не классифицируется как точное совпадение, содержащее только целые рекламные акции, преобразования из floatdouble, а тривиальные преобразования классифицируются как совпадение с использованием рекламных акций. Хотя сопоставление с использованием повышений не такое хорошее, как точное, оно лучше сопоставления с использованием стандартных преобразований.

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

    • Преобразование из указателя в производный класс к указателю на прямой или косвенный базовый класс предпочтительнее преобразовать в void * или const void *.

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

Example class hierarchy showing that class A inherits from B which inherits from C which inherits from D.
Диаграмма, показывающая предпочитаемые преобразования.

Преобразование из типа D* в тип C* предпочтительнее преобразования из типа D* в тип B*. Аналогично, преобразование из типа D* в тип B* предпочтительнее преобразования из типа D* в тип A*.

Это же правило применяется для преобразований ссылок. Преобразование из типа D& в тип C& предпочтительнее преобразования из типа D& в тип B& и т. д.

Это же правило применяется для преобразований указателей на член. Преобразование из типа T D::* в тип T C::* предпочтительнее преобразования из типа T D::* в тип T B::* и т. д. (T — тип члена.)

Предыдущее правило применяется только в определенном пути наследования. Рассмотрим граф, показанный на следующем рисунке.

Diagram of multiple inheritance that shows preferred conversions. Class C is the base class of class B and D. Class A inherits from class B
Диаграмма с несколькими наследованиеми, показывающая предпочитаемые преобразования.

Преобразование из типа C* в тип B* предпочтительнее преобразования из типа C* в тип A*. Причина заключается в том, что эти преобразования находятся на одном пути и узел B* ближе. Однако преобразование из типа в тип не предпочтительнее для преобразования в тип C*D*A*; нет предпочтений, так как преобразования следуют разным путям.

  1. Сопоставление с пользовательскими преобразованиями. Эта последовательность не может быть классифицирована как точное совпадение, совпадение с использованием рекламных акций или совпадения с использованием стандартных преобразований. Чтобы классифицироваться как совпадение с пользовательскими преобразованиями, последовательность должна содержать только определяемые пользователем преобразования, стандартные преобразования или тривиальные преобразования. Совпадение с определяемыми пользователем преобразованиями считается лучшим, чем совпадение с многоточием (...), но не так хорошо, как совпадение со стандартными преобразованиями.

  2. Сопоставление с многоточием. Любая последовательность, соответствующая многоточию в объявлении, классифицируется как сопоставление с многоточием. Считается самым слабым матчем.

Пользовательские преобразования применяются при отсутствии встроенного повышения или преобразования. Эти преобразования выбираются на основе типа соответствующего аргумента. Рассмотрим следующий код:

// argument_matching1.cpp
class UDC
{
public:
   operator int()
   {
      return 0;
   }
   operator long();
};

void Print( int i )
{
};

UDC udc;

int main()
{
   Print( udc );
}

Доступные пользовательские преобразования для класса UDC относятся к типу и типу intlong. Поэтому компилятор проверяет преобразования для типа сопоставляемого объекта: UDC. Преобразование, необходимое для int существования, и выбрано.

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

void LogToFile( long l );
...
UDC udc;
LogToFile( udc );

В этом примере компилятор вызывает определяемое пользователем преобразование, operator longчтобы преобразовать udc в тип long. Если определяемое пользователем преобразование в тип long не определено, компилятор сначала преобразует тип в тип UDCint с помощью определяемого operator int пользователем преобразования. Затем оно будет применять стандартное преобразование из типа в тип intlong , чтобы соответствовать аргументу в объявлении.

Если для сопоставления аргумента требуются какие-либо определяемые пользователем преобразования, стандартные преобразования не используются при оценке наилучшего соответствия. Даже если для нескольких кандидатов требуется преобразование, определяемое пользователем, функции считаются равными. Например:

// argument_matching2.cpp
// C2668 expected
class UDC1
{
public:
   UDC1( int );  // User-defined conversion from int.
};

class UDC2
{
public:
   UDC2( long ); // User-defined conversion from long.
};

void Func( UDC1 );
void Func( UDC2 );

int main()
{
   Func( 1 );
}

Обе версии Func требуют преобразования, определяемого пользователем, для преобразования типа int в аргумент типа класса. Возможные преобразования:

  • Преобразование из типа в тип intUDC1 (определяемое пользователем преобразование).

  • Преобразование из типа в тип intlong; затем преобразование в тип UDC2 (двухфакторное преобразование).

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

Примечание.

Определяемые пользователем преобразования считаются преобразованием путем построения или преобразования путем инициализации. Компилятор считает оба метода равными, когда он определяет лучшее совпадение.

Сопоставление аргументов this и указатель

Функции-члены класса обрабатываются по-разному в зависимости от того, объявляются ли они как static. static Функции не имеют неявного аргумента, который предоставляет this указатель, поэтому они считаются одним менее аргументом, чем обычные функции-члены. В противном случае они объявляются одинаково.

Функции-члены, которые не static требуют подразумеваемого this указателя, чтобы соответствовать типу объекта, который вызывается функцией. Кроме того, для перегруженных операторов требуется первый аргумент для сопоставления объекта, к которым применяется оператор. Дополнительные сведения о перегруженных операторах см. в разделе "Перегруженные операторы".

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

-> Если оператор выбора члена используется для доступа к функции-члену классаclass_name, this аргумент указателя имеет типclass_name * const. Если члены объявлены как const или volatile, типы и const class_name * constvolatile class_name * constсоответственно.

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

// Expression encountered in code
obj.name

// How the compiler treats it
(&obj)->name

С точки зрения сопоставления аргументов, левый операнд операторов ->* и .* (указатель на член) обрабатывается так же, как и для операторов . и -> (выбор члена).

Квалификаторы ссылок на функции-члены

Ссылочные квалификаторы позволяют перегружать функцию-член на основе того, указывает this ли объект на rvalue или lvalue. Используйте эту функцию, чтобы избежать ненужных операций копирования в сценариях, где вы решили не предоставлять указатель доступа к данным. Например, предположим, что класс C инициализирует некоторые данные в конструкторе и возвращает копию этих данных в функции-члене get_data(). Если объект типа C является rvalue, который будет уничтожен, компилятор выбирает перегрузку, которая перемещается get_data() && вместо копирования данных.

#include <iostream>
#include <vector>

using namespace std;

class C
{
public:
    C() {/*expensive initialization*/}
    vector<unsigned> get_data() &
    {
        cout << "lvalue\n";
        return _data;
    }
    vector<unsigned> get_data() &&
    {
        cout << "rvalue\n";
        return std::move(_data);
    }

private:
    vector<unsigned> _data;
};

int main()
{
    C c;
    auto v = c.get_data(); // get a copy. prints "lvalue".
    auto v2 = C().get_data(); // get the original. prints "rvalue"
    return 0;
}

Ограничения на перегрузку

К допустимому набору перегруженных функций применяется несколько ограничений.

  • Любые две функции в наборе перегруженных функций должны иметь разные списки аргументов.

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

    Блок, относящийся только к системам Майкрософт

    Вы можете перегружаться operator new на основе возвращаемого типа, в частности, на основе модификатора модели памяти.

    Завершение блока, относящегося только к системам Майкрософт

  • Функции-члены не могут быть перегружены исключительно потому, что один из них static и другой не staticявляется.

  • typedef объявления не определяют новые типы; они вводят синонимы для существующих типов. Они не влияют на механизм перегрузки. Рассмотрим следующий код:

    typedef char * PSTR;
    
    void Print( char *szToPrint );
    void Print( PSTR szToPrint );
    

    Две указанные выше функции имеют идентичные списки аргументов. PSTR является синонимом типа char *. В области члена этот код возвращает ошибку.

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

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

    void Print( char *szToPrint );
    void Print( char szToPrint[] );
    

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

    void Print( char szToPrint[] );
    void Print( char szToPrint[][7] );
    void Print( char szToPrint[][9][42] );
    

Перегрузка, переопределение и скрытие

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

Класс область строго наблюдается. Функция, объявленная в базовом классе, не имеет того же область, что и функция, объявленная в производном классе. Если функция в производном классе объявляется с тем же именем, что virtual и функция в базовом классе, функция производного класса переопределяет функцию базового класса. Дополнительные сведения см. в разделе "Виртуальные функции".

Если функция базового класса не объявлена как virtual, то производная функция класса, как утверждается , скрывает ее. Переопределение и скрытие отличаются от перегрузки.

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

// declaration_matching1.cpp
// compile with: /EHsc
#include <iostream>

using namespace std;
void func( int i )
{
    cout << "Called file-scoped func : " << i << endl;
}

void func( char *sz )
{
    cout << "Called locally declared func : " << sz << endl;
}

int main()
{
    // Declare func local to main.
    extern void func( char *sz );

    func( 3 );   // C2664 Error. func( int ) is hidden.
    func( "s" );
}

В предыдущем коде показаны два определения функции func. Определение, которое принимает аргумент типа char * , является локальным main из-за инструкции extern . Таким образом, определение, которое принимает аргумент типа int , скрыто, и первый вызов func находится в ошибке.

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

Целью кода в примере является предоставление класса Account, в котором для внесения средств требуется правильный пароль. Это делается с помощью перегрузки.

Вызов Deposit при Account::Deposit вызове функции частного члена. Этот вызов является правильным, так как Account::Deposit является функцией-членом и имеет доступ к частным членам класса.

// declaration_matching2.cpp
class Account
{
public:
   Account()
   {
   }
   double Deposit( double dAmount, char *szPassword );

private:
   double Deposit( double dAmount )
   {
      return 0.0;
   }
   int Validate( char *szPassword )
   {
      return 0;
   }

};

int main()
{
    // Allocate a new object of type Account.
    Account *pAcct = new Account;

    // Deposit $57.22. Error: calls a private function.
    // pAcct->Deposit( 57.22 );

    // Deposit $57.22 and supply a password. OK: calls a
    //  public function.
    pAcct->Deposit( 52.77, "pswd" );
}

double Account::Deposit( double dAmount, char *szPassword )
{
   if ( Validate( szPassword ) )
      return Deposit( dAmount );
   else
      return 0.0;
}

См. также

Функции (C++)