Главная страница
Навигация по странице:

  • Еще раз о разрешении перегрузки функций

  • 15.10.2. Функции-кандидаты

  • 15.10.3. Функции-кандидаты для вызова функции в области видимости класса

  • 15.10.4. Ранжирование последовательностей определенных пользователем преобразований

  • 15.11.1. Объявления перегруженных функций-членов

  • Язык программирования C++. Вводный курс. С для начинающих


    Скачать 5.41 Mb.
    НазваниеС для начинающих
    Дата24.08.2022
    Размер5.41 Mb.
    Формат файлаpdf
    Имя файлаЯзык программирования C++. Вводный курс.pdf
    ТипДокументы
    #652350
    страница72 из 93
    1   ...   68   69   70   71   72   73   74   75   ...   93
    753
    использование конструкторов при выполнении неявных преобразований (а значит, уменьшить вероятность неожиданных эффектов) можно путем объявления их явными.
    15.10.1.
    Еще раз о разрешении перегрузки функций
    В главе 9 подробно описывалось, как разрешается вызов перегруженной функции. Если фактические аргументы при вызове имеют тип класса, указателя на тип класса или указателя на члены класса, то на роль возможных кандидатов претендует большее число функций. Следовательно, наличие таких аргументов оказывает влияние на первый шаг процедуры разрешения перегрузки – отбор множества функций-кандидатов.
    На третьем шаге этой процедуры выбирается наилучшее соответствие. При этом ранжируются преобразования типов фактических аргументов в типы формальных параметров функции. Если аргументы и параметры имеют тип класса, то в множество возможных преобразований следует включать и последовательности определенных пользователем преобразований, также подвергая их ранжированию.
    В этом разделе мы детально рассмотрим, как фактические аргументы и формальные параметры типа класса влияют на отбор функций-кандидатов и как последовательности определенных пользователем преобразований сказываются на выборе наилучшей из устоявших функции.
    15.10.2.
    Функции-кандидаты
    Функцией-кандидатом называется функция с тем же именем, что и вызванная.
    Предположим, что имеется такой вызов: add( si, 566 );
    Функция-кандидат должна иметь имя add. Какие из объявлений add() принимаются во внимание? Те, которые видимы в точке вызова.
    Например, обе функции add(), объявленные в глобальной области видимости, будут кандидатами для следующего вызова:
    }
    Рассмотрение функций, чьи объявления видны в точке вызова, производится не только для вызовов с аргументами типа класса. Однако для них поиск объявлений проводится еще в двух областях видимости:

    если фактический аргумент – это объект типа класса, указатель или ссылка на тип класса либо указатель на член класса и этот тип объявлен в пользовательском
    SmallInt si(15); const matrix& add( const matrix &, int ); double add( double, double ); int main() {
    SmallInt si(15); add( si, 566 );
    // ...

    С++ для начинающих
    754
    пространстве имен, то к множеству функций-кандидатов добавляются функции, объявленные в этом же пространстве и имеющие то же имя, что и вызванная:
    }
    Аргумент si имеет тип SmallInt, т.е. тип класса, объявленного в пространстве имен
    NS
    . Поэтому к множеству функций-кандидатов добавляется add(const String &, const String &)
    , объявленная в этом пространстве имен;

    если фактический аргумент – это объект типа класса, указатель или ссылка на класс либо указатель на член класса и у этого класса есть друзья, имеющие то же имя, что и вызванная функция, то они добавляются к множеству функций-кандидатов:
    }
    Аргумент функции si имеет тип SmallInt. Функция-друг класса SmallInt add(SmallInt, int)
    – член пространства имен NS, хотя непосредственно в этом пространстве она не объявлена. При обычном поиске в NS функция-друг не будет найдена. Однако при вызове add() с аргументом типа класса SmallInt принимаются во внимание и добавляются к множеству кандидатов также друзья этого класса, объявленные в списке его членов.
    Таким образом, если в списке фактических аргументов функции есть объект, указатель или ссылка на класс, а также указатели на члены класса, то множество функций- кандидатов состоит из множества функций, видимых в точке вызова, или объявленных в том же пространстве имен, где определен тип класса, или объявленных друзьями этого класса.
    Рассмотрим следующий пример: namespace NS { class SmallInt { /* ... */ }; class String { /* ... */ };
    String add( const String &, const String & );
    } int main() {
    // si имеет тип class SmallInt:
    // класс объявлен в пространстве имен NS
    NS::SmallInt si(15); add( si, 566 ); // NS::add() - функция-кандидат return 0; namespace NS { class SmallInt { friend SmallInt add( SmallInt, int ) { /* ... */ }
    };
    } int main() {
    NS::SmallInt si(15); add( si, 566 ); // функция-друг add() - кандидат return 0;

    С++ для начинающих
    755
    }
    Здесь кандидатами являются:

    глобальные функции: double add( double, double )

    функция из пространства имен:
    NS::add( const String &, const String & )

    функция-друг:
    NS::add( SmallInt, int )
    При разрешении перегрузки выбирается функция-друг класса SmallInt NS::add(
    SmallInt, int )
    как наилучшая из устоявших: оба фактических аргумента точно соответствуют заданным формальным параметрам.
    Разумеется, вызванная функция может быть несколько аргументов типа класса, указателя или ссылки на класс либо указателя на член класса. Допускаются разные типы классов для каждого из таких аргументов. Поиск функций-кандидатов для них ведется в пространстве имен, где определен класс, и среди функций-друзей класса. Поэтому результирующее множество кандидатов для вызова функции с такими аргументами содержит функции из разных пространств имен и функции-друзья, объявленные в разных классах.
    15.10.3.
    Функции-кандидаты для вызова функции в области
    видимости класса
    Когда вызов функции вида namespace NS { class SmallInt { friend SmallInt add( SmallInt, int ) { /* ... */ }
    }; class String { /* ... */ };
    String add( const String &, const String & );
    } const matrix& add( const matrix &, int ); double add( double, double ); int main() {
    // si имеет тип class SmallInt:
    // класс объявлен в пространстве имен NS
    NS::SmallInt si(15); add( si, 566 ); // вызывается функция-друг return 0; const matrix& add( const matrix &, int )

    С++ для начинающих
    756
    calc(t) встречается в области видимости класса (например, внутри функции-члена), то первая часть множества кандидатов, описанного в предыдущем подразделе (т.е. множество, включающее объявления функций, видимых в точке вызова), может содержать не только функции-члены класса. Для построения такого множества применяется разрешение имени. (Эта тема детально разбиралась в разделах 13.9 – 13.12.)
    Рассмотрим пример:
    }
    Как отмечалось в разделе 13.11, квалификаторы NS::myClass:: просматриваются в обратном порядке: сначала поиск видимого объявления для имени, использованного в определении функции-члена mf(), ведется в классе myClass, а затем – в пространстве имен NS. Рассмотрим первый вызов: h( 'a' );
    При разрешении имени h() в определении функции-члена mf() сначала просматриваются функции-члены myClass. Поскольку функции-члена с таким именем в области видимости этого класса нет, то далее поиск идет в пространстве имен NS.
    Функции h()нет и там, поэтому мы переходим в глобальную область видимости.
    Результат – глобальная функция h(char), единственная функция-кандидат, видимая в точке вызова.
    Как только найдено подходящее объявление, поиск прекращается. Следовательно, множество содержит только те функции, объявления которых находятся в областях видимости, где разрешение имени завершилось успешно. Это можно наблюдать на примере построения множества кандидатов для вызова k( 4 );
    Сначала поиск ведется в области видимости класса myClass. При этом найдены две функции-члена k(int) и k(char*). Поскольку множество кандидатов содержит лишь функции, объявленные в той области, где разрешение успешно завершилось, то пространство имен NS не просматривается и функция k(double) в данное множество не включается. namespace NS { struct myClass { void k( int ); static void k( char* ); void mf();
    }; int k( double );
    }; void h(char); void NS::myClass::mf() { h('a'); // вызывается глобальная h( char ) k(4); // вызывается myClass::k( int )

    С++ для начинающих
    757
    Если обнаруживается, что вызов неоднозначен, поскольку в множестве нет наиболее подходящей функции, то компилятор выдает сообщение об ошибке. Поиск кандидатов, лучше соответствующих фактическим аргументам, в объемлющих областях видимости не производится.
    15.10.4.
    Ранжирование последовательностей определенных
    пользователем преобразований
    Фактический аргумент функции может быть неявно приведен к типу формального параметра с помощью последовательности определенных пользователем преобразований.
    Как это влияет на разрешение перегрузки? Например, если имеется следующий вызов calc()
    , то какая функция будет вызвана?
    }
    Выбирается функция, формальные параметры которой лучше всего соответствуют типам фактических аргументов. Она называется лучшим соответствием или наилучшей из устоявших функций. Для выбора такой функции неявные преобразования, примененные к фактическим аргументам, подвергаются ранжированию. Лучшей из устоявших считается та, для которой примененные к аргументам изменения не хуже, чем для любой другой устоявшей, а хотя бы для одного аргумента они лучше, чем для всех остальных функций.
    Последовательность стандартных преобразований всегда лучше последовательности определенных пользователем преобразований. Так, при вызове calc() из примера выше обе функции calc() являются устоявшими. calc(double) устояла потому, что существует стандартное преобразование типа фактического аргумента int в тип формального параметра double, а calc(SmallInt) – потому, что имеется определенное пользователем преобразование из int в SmallInt, которое использует конструктор
    SmallInt(int)
    . Следовательно, наилучшей из устоявших функций будет calc(double).
    А как сравниваются две последовательности определенных пользователем преобразований? Если в них используются разные конвертеры или разные конструкторы, то обе такие последовательности считаются одинаково хорошими: class SmallInt { public:
    SmallInt( int );
    }; extern void calc( double ); extern void calc( SmallInt ); int ival; int main() { calc( ival ); // какая calc() вызывается?

    С++ для начинающих
    758
    calc( num ); // ошибка: неоднозначность
    Устоявшими окажутся и calc(int), и calc(SmallInt); первая – поскольку конвертер
    Number::operator int()
    преобразует фактический аргумент типа Number в формальный параметр типа int, а вторая потому, что конвертер Number::operator SmallInt() преобразует фактический аргумент типа Number в формальный параметр типа
    SmallInt
    . Так как последовательности определенных пользователем преобразований всегда имеют одинаковый ранг, то компилятор не может выбрать, какая из них лучше.
    Таким образом, этот вызов функции неоднозначен и приводит к ошибке компиляции.
    Есть способ разрешить неоднозначность, указав преобразование явно: calc( static_cast< int >( num ) );
    Явное приведение типов заставляет компилятор преобразовать аргумент num в тип int с помощью конвертера Number::operator int(). Фактический аргумент тогда будет иметь тип int, что точно соответствует функции calc(int), которая и выбирается в качестве наилучшей.
    Допустим, в классе Number не определен конвертер Number::operator int(). Будет ли тогда вызов calc( num ); // по-прежнему неоднозначен? по-прежнему неоднозначен? Вспомните, что в SmallInt также есть конвертер, способный преобразовать значение типа SmallInt в int.
    };
    Можно предположить, что функция calc() вызывается, если сначала преобразовать фактический аргумент num из типа Number в тип SmallInt с помощью конвертера class Number { public: operator SmallInt(); operator int();
    // ...
    }; extern void calc( int ); extern void calc( SmallInt ); extern Number num;
    // явное указание преобразования устраняет неоднозначность
    // определен только Number::operator SmallInt() class SmallInt { public: operator int();
    // ...

    С++ для начинающих
    759
    Number::operator SmallInt()
    , а затем результат привести к типу int с помощью
    SmallInt::operator
    SmallInt()
    Однако это не так. Напомним, что в последовательность определенных пользователем преобразований может входит несколько стандартных преобразований, но лишь одно пользовательское. Если конвертер
    Number::operator int()
    не определен, то функция calc(int) не считается устоявшей, поскольку не существует неявного преобразования из типа фактического аргумента num в тип формального параметра int.
    Поэтому в отсутствие конвертера Number::operator int() единственной устоявшей функцией будет calc(SmallInt), в пользу которой и разрешается вызов.
    Если в двух последовательностях определенных пользователем преобразований употребляется один и тот же конвертер, то выбор наилучшей зависит от последовательности стандартных преобразований, выполняемых после его вызова:
    };
    }
    Как manip(int), так и manip(char) являются устоявшими функциями; первая – потому, что конвертер SmallInt::operator int() преобразует фактический аргумент типа
    SmallInt в тип формального параметра int, а вторая – потому, что тот же конвертер преобразует SmallInt в int, после чего результат с помощью стандартного преобразования приводится к типу char. Последовательности определенных пользователем преобразований выглядят так: manip(int) : operator int()->
    стандартное преобразование
    Поскольку в обеих последовательностях используется один и тот же конвертер, то для определения лучшей из них анализируется ранг последовательности стандартных преобразований. Так как точное соответствие лучше преобразования, то наилучшей из устоявших будет функция manip(int).
    Подчеркнем, что такой критерий выбора принимается только тогда, когда в обеих последовательностях определенных пользователем преобразований применяется один и тот же конвертер. Этим наш пример отличается от приведенных в конце раздела 15.9, где мы показывали, как компилятор выбирает пользовательское преобразование некоторого значения в данный целевой тип: исходный и целевой типы были фиксированы, и компилятору приходилось выбирать между различными определенными пользователем class SmallInt { public: operator int();
    // ... void manip( int ); void manip( char );
    SmallInt si ( 68 ); main() { manip( si ); // вызывается manip( int ) manip(int) : operator int()->
    точное соответствие

    С++ для начинающих
    760
    преобразованиями одного типа в другой. Здесь же рассматриваются две разных функции с разными типами формальных параметров, и целевые типы отличаются. Если для двух разных типов параметров нужны различные определенные пользователем преобразования, то предпочесть один тип другому возможно только в том случае, когда в обеих последовательностях используется один и тот же конвертер. Если это не так, то для выбора наилучшего целевого типа оцениваются стандартные преобразования, следующие за применением конвертера. Например:
    };
    }
    И compute(float), и compute(int) – устоявшие функции. compute(float) – потому, что конвертер SmallInt::operator float()преобразует аргумент типа SmallInt в тип параметра float, а compute(char) – потому, что SmallInt::operator int() преобразует аргумент типа SmallInt в тип int, после чего результат стандартно приводится к типу char. Таким образом, имеются последовательности: compute(char) : operator char()->
    стандартное преобразование
    Поскольку в них применяются разные конвертеры, то невозможно определить, у какой функции формальные параметры лучше соответствуют вызову. Для выбора лучшей из двух ранг последовательности стандартных преобразований не используется. Вызов помечается компилятором как неоднозначный.
    Упражнение 15.12
    В классах стандартной библиотеки C++ нет определений конвертеров, а большинство конструкторов, принимающих один параметр, объявлены явными. Однако определено множество перегруженных операторов. Как вы думаете, почему при проектировании было принято такое решение?
    Упражнение 15.13
    Почему перегруженный оператор ввода для класса SmallInt, определенный в начале этого раздела, реализован не так: class SmallInt { public: operator int(); operator float();
    // ... void compute( float ); void compute( char );
    SmallInt si ( 68 ); main() { compute( si ); // неоднозначность compute(float) : operator float()->
    точное соответствие

    С++ для начинающих
    761
    }
    Упражнение 15.14
    Приведите возможные последовательности определенных пользователем преобразований для следующих инициализаций. Каким будет результат каждой инициализации? extern LongDouble ldObj;
    (b) float ex2 = ldObj;
    Упражнение 15.15
    Назовите три множества функций-кандидатов, рассматриваемых при разрешении перегрузки функции в случае, когда хотя бы один ее аргумент имеет тип класса.
    Упражнение 15.16
    Какая из функций calc() выбирается в качестве наилучшей из устоявших в данном случае? Покажите последовательности преобразований, необходимых для вызова каждой функции, и объясните, почему одна из них лучше другой.
    } istream& operator>>( istream &is, SmallInt &si )
    { return ( is >> is.value ); class LongDouble { operator double(); operator float();
    };
    (a) int ex1 = ldObj; class LongDouble { public:
    LongDouble( double );
    // ...
    }; extern void calc( int ); extern void calc( LongDouble ); double dval; int main() { calc( dval ); // какая функция?

    С++ для начинающих
    762
    15.11.
    Разрешение перегрузки и функции-члены A
    Функции-члены также могут быть перегружены, и в этом случае тоже применяется процедура разрешения перегрузки для выбора наилучшей из устоявших. Такое разрешение очень похоже на аналогичную процедуру для обычных функций и состоит из тех же трех шагов:
    1. Отбор функций-кандидатов.
    2. Отбор устоявших функций.
    3. Выбор наилучшей из устоявших функции.
    Однако есть небольшие различия в алгоритмах формирования множества кандидатов и отбора устоявших функций-членов. Эти различия мы и рассмотрим в настоящем разделе.
    15.11.1.
    Объявления перегруженных функций-членов
    Функции-члены класса можно перегружать:
    };
    Как и в случае функций, объявленных в пространстве имен, функции-члены могут иметь одинаковые имена при условии, что списки их параметров различны либо по числу параметров, либо по их типам. Если же объявления двух функций-членов отличаются только типом возвращаемого значения, то второе объявление считается ошибкой компиляции:
    };
    В отличие от функций в пространствах имен, функции-члены должны быть объявлены только один раз. Если даже тип возвращаемого значения и списки параметров двух функций-членов совпадают, то второе объявление компилятор трактует как неверное повторное объявление: class myClass { public: void f( double ); char f( char, char ); // перегружает myClass::f( double )
    // ... class myClass { public: void mf(); double mf(); // ошибка: так перегружать нельзя
    // ... class myClass { public: void mf(); void mf(); // ошибка: повторное объявление
    // ...

    С++ для начинающих
    1   ...   68   69   70   71   72   73   74   75   ...   93


    написать администратору сайта