С++ для начинающих

Разрешение перегрузки при конкретизации *


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

// шаблон функции

template <class Type>

   Type sum( Type, int ) { /* ... */ }

// обычная функция (не шаблон)

double sum( double, double );

Когда программа обращается к sum(), вызов разрешается либо в пользу конкретизированного экземпляра шаблона, либо в пользу обычной функции – это зависит от того, какая функция лучше соответствует фактическим аргументам. (Для решения такой проблемы применяется процесс разрешения перегрузки, описанный в главе 9.) Рассмотрим следующий пример:

void calc( int ii, double dd ) {

   // что будет вызвано: конкретизированный экземпляр шаблона

   // или обычная функция?

   sum( dd, ii );

}

Будет ли при обращении к sum(dd,ii) вызвана функция, конкретизированная из шаблона, или обычная функция? Чтобы ответить на этот вопрос, выполним по шагам процедуру разрешения перегрузки. Первый шаг заключается в построении множества функций-кандидатов состоящего из одноименных вызванной функций, объявления которых видны в точке вызова.



Если существует шаблон функции и на основе фактических аргументов вызова из него может быть конкретизирована функция, то она будет являться кандидатом. Так ли это на самом деле, зависит от результата процесса вывода аргументов шаблона. (Этот процесс описан в разделе 10.3.) В предыдущем примере для вывода значения аргумента Type шаблона используется фактический аргумент функции dd. Тип выведенного аргумента оказывается равным double, и к множеству функций-кандидатов добавляется функция sum(double, int). Таким образом, для данного вызова имеются два кандидата: конкретизированная из шаблона функция sum(double, int) и обычная функция sum(double, double).

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

Второй шаг процедуры разрешения перегрузки заключается в выборе устоявших функций из множества кандидатов. Напомним, что устоявшей называется функция, для которой существуют преобразования типов, приводящие каждый фактический аргумент функции к типу соответствующего формального параметра. (В разделе 9.3 описаны преобразования типов, применимые к фактическим аргументам функции.) Нужные трансформации существуют как для конкретизированной функции sum(double, int), так и для обычной функции sum(double, double). Следовательно, обе они являются устоявшими.


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

Для конкретизированной из шаблона функции sum(double, int):

  • для первого фактического аргумента как сам этот аргумент, так и формальный параметр имеют тип double, т.е. мы видим точное соответствие;


  • для второго фактического аргумента как сам аргумент, так и формальный параметр имеют тип int, т.е. снова точное соответствие.


  • Для обычной функции sum(double, double):

  • для первого фактического аргумента как сам этот аргумент, так и формальный параметр имеют тип double – точное соответствие;


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


  • Если рассматривать только первый аргумент, то обе функции одинаково хороши. Однако для второго аргумента конкретизированная из шаблона функция лучше. Поэтому наиболее подходящей (лучшей из устоявших) считается функция sum(double, int).

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

    // шаблон функции

    template <class T>

       int sum( T*, int ) { ... }

    Для описанного вызова функции вывод аргументов шаблона будет неудачным, так как фактический аргумент типа double не может соответствовать формальному параметру типа T*. Поскольку для данного вызова и данного шаблона конкретизировать функцию невозможно, в множество кандидатов ничего не добавляется, т.е. единственным его элементом останется обычная функция sum(double, double). Именно она вызывается при обращении, и ее второй фактический аргумент приводится к типу double.

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



    // определение шаблона функции

    template <class Type> Type sum( Type, int ) { /* ... */ }

    // явная специализация для Type == double

    template<> double sum<double>( double,int );

    // обычная функция

    double sum( double, double );

    void manip( int ii, double dd ) {

       // вызывается явная специализация шаблона sum<double>()

       sum( dd, ii );

    }

    При обращении к sum() внутри manip() в процессе вывода аргументов шаблона обнаруживается, что функция sum(double,int), конкретизированная из обобщенного шаблона, должна быть добавлена к множеству кандидатов. Но для нее имеется явная специализация, которая и становится кандидатом. На более поздних стадиях анализа выясняется, что эта специализация дает наилучшее соответствие фактическим аргументам вызова, так что разрешение перегрузки завершается в ее пользу.

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

    // определение шаблона функции

    template <class Type>

       Type min( Type, Type ) { /* ... */ }

    // явная специализация для Type == double

    template<> double min<double>( double, double );

    void manip( int ii, double dd ) {

       // ошибка: вывод аргументов шаблона неудачен,

       // нет функций-кандидатов для данного вызова

       min( dd, ii );

    }

    Шаблон функции min() специализирован для аргумента double. Однако эта специализация не попадает в множество функций-кандидатов. Процесс вывода для вызова min() завершился неудачно, поскольку аргументы шаблона, выведенные для Type на основе разных фактических аргументов функции, оказались различными: для первого аргумента выводится тип double, а для второго – int. Поскольку вывести аргументы не удалось, в множество кандидатов никакая функция не добавляется, и специализация min(double, double) игнорируется. Так как других функций-кандидатов нет, вызов считается ошибочным.



    Как отмечалось в разделе 10.6, тип возвращаемого значения и список формальных параметров обычной функции может точно соответствовать аналогичным атрибутам функции, конкретизированной из шаблона. В следующем примере min(int,int) – это обычная функция, а не специализация шаблона min(), поскольку, как вы, вероятно, помните, объявление специализации должно начинаться с template<>:

    // объявление шаблона функции

    template <class T>

       T min( T, T );

    // обычная функция min(int,int)

    int min( int, int ) { }

    Вызов может точно соответствовать как обычной функции, так и функции, конкретизированной из шаблона. В следующем примере оба аргумента в min(ai[0],99) имеют тип int. Для этого вызова есть две устоявших функции: обычная min(int,int) и конкретизированная из шаблона функция с тем же типом возвращаемого значения и списком параметров:

    int ai[4] = { 22, 33, 44, 55 };

    int main() {

       // вызывается обычная функция min( int, int )

       min( ai[0], 99 );

    }

    Однако такой вызов не является неоднозначным. Обычной функции, если она существует, всегда отдается предпочтение, поскольку она реализована явно, так что перегрузка разрешается в пользу обычной функции min(int,int).

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

    // шаблон функции

    template <class T>

       T min( T, T ) { ... }

    // это обычная функция, не определенная в программе

    int min( int, int );

    int ai[4] = { 22, 33, 44, 55 };

    int main() {

       // ошибка сборки: min( int, int ) не определена

       min( ai[0], 99 );

    }

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



    Предположим, что мы хотим определить специализацию шаблона функции min<int>(int,int). Нужно, чтобы именно эта функция вызывалась при обращении к min() с аргументами любых целых типов, пусть даже неодинаковых. Из-за ограничений, наложенных на преобразования типов, при передаче фактических аргументов разных типов функция min<int>(int,int) не будет конкретизирована из шаблона. Мы могли бы заставить компилятор выполнить конкретизацию, явно задав аргументы шаблона, однако решение, при котором не требуется модифицировать каждый вызов, предпочтительнее. Определив обычную функцию, мы добьемся того, что программа будет вызывать специальную версию min(int,int) для любых фактических аргументов целых типов без явного указания аргументов шаблона:

    // определение шаблона функции

    template <class Type>

       Type min( Type t1, Type t2 ) { ... }

    int ai[4] = { 22, 33, 44, 55 };

    short ss = 88;

    void call_instantiation() {

       // ошибка: для этого вызова нет функции-кандидата

       min( ai[0], ss );

    }

    // обычная функция

    int min( int a1, int a2 ) {

       min<int>( a1, a2 );

    }

    int main() {

       call_instantiation() {

       // вызывается обычная функция

       min( ai[0], ss );

    }

    Для вызова min(ai[0],ss) из call_instantiation нет ни одной функции-кандидата. Попытка сгенерировать ее из шаблона min() провалится, поскольку для аргумента шаблона Type из фактических аргументов функции выводятся два разных значения. Следовательно, такой вызов ошибочен. Однако при обращении к min(ai[0],ss) внутри main() видимо объявление обычной функции min(int, int). Тип первого фактического аргумента этой функции точно соответствует типу формального параметра, а второй аргумент может быть преобразован в тип формального параметра с помощью расширения типа. Поскольку для второго вызова устояла только данная функция, то она и вызывается.

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



    1.      Построить множество функций-кандидатов.

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

    2.      Построить множество устоявших функций (см. раздел 9.3).

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

    3.      Ранжировать преобразования типов (см. раздел 9.3).

    a.   Если есть только одна функция, вызвать именно ее.

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

    4.      Разрешить перегрузку, рассматривая среди всех устоявших только обычные функции (см. раздел 9.3).

    a.   Если есть только одна функция, вызвать именно ее.

    b.   В противном случае вызов неоднозначен.

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

    template <class Type>

       Type max( Type, Type ) { ... }

    // обычная функция

    double max( double, double );

    А вот три вызова max(). Можете ли вы сказать, какая функция будет вызвана в каждом случае?

    int main() {

       int ival;

       double dval;

       float fd;

       // ival, dval и fd присваиваются значения

       max( 0, ival );

       max( 0.25, dval );

       max( 0, fd );

    }

    Рассмотрим последовательно все три вызова:

    1.      max(0,ival). Оба аргумента имеют тип int. Для вызова есть два кандидата: конкретизированная из шаблона функция max(int, int) и обычная функция max(double, double). Конкретизированная функция точно соответствует фактическим аргументам, поэтому она и вызывается;

    2.      max(0.25,double). Оба аргумента имеют тип double. Для вызова есть два кандидата: конкретизированная из шаблона max(double, double) и обычная max(double, double). Вызов неоднозначен, поскольку точно соответствует обеим функциям. Правило 3b говорит, что в таком случае выбирается обычная функция;.



    3.      max(0,fd). Аргументы имеют тип int и float соответственно. Для вызова существует только один кандидат: обычная функция max(double, double). Вывод аргументов шаблона заканчивается неудачей, так как значения типа Type, выведенные из разных фактических аргументов функции, различны. Поэтому в множество кандидатов конкретизированная из шаблона функция не попадает. Обычная же функция устояла, поскольку существуют преобразования типов фактических аргументов в типы формальных параметров; она и выбирается. Если бы обычная функция не была объявлена, вызов закончился бы ошибкой.

    А если бы мы определили еще одну обычную функцию для max()? Например:

    template <class T> T max( T, T ) { ... }

    // две обычные функции

    char max( char, char );

    double max( double, double );

    Будет ли в таком случае третий вызов разрешен по-другому? Да.

    int main() {

       float fd;

       // в пользу какой функции разрешается вызов?

       max( 0, fd );

    }

    Правило 3b говорит, что, поскольку вызов неоднозначен, следует рассматривать только обычные функции. Ни одна из них не считается наилучшей из устоявших, так как преобразования типов фактических аргументов одинаково плохи: в обоих случаях для установления соответствия требуется стандартная трансформация. Таким образом, вызов неоднозначен, и компилятор сообщает об ошибке.

    Упражнение 10.11

    Вернемся к представленному ранее примеру:

    template <class Type>

       Type max( Type, Type ) { ... }

    double max( double, double );

    int main() {

       int ival;

       double dval;

       float fd;

       max( 0, ival );

       max( 0.25, dval );

       max( 0, fd );

    }

    Добавим в множество объявлений в глобальной области видимости следующую специализацию шаблона функции:

    template <> char max<char>* char, char ) { ... }

    Составьте список кандидатов и устоявших функций для каждого вызова max() внутри main().

    Предположим, что в main() добавлен следующий вызов:

    int main() {

       // ...

       max( 0, 'j' );

    }

    В пользу какой функции он будет разрешен? Почему?

    Упражнение 10.12

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

    int i;             unsigned int ui;

    char str[24];      int ia[24];

    template <class T> T calc( T*, int );

    template <class T> T calc( T, T );

    template<> chat calc( char*. int );

    double calc( double, double );

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

    (a) cslc( str, 24 );        (d) calc( i, ui );

    (b) calc( is, 24 );         (e) calc( ia, ui );

    (c) calc( ia[0], 1 );       (f) calc( &i, i );


    Содержание раздела