Б. Страуструп - Дизайн и Эволюция C++. 2006 (1160775), страница 23
Текст из файла (страница 23)
раздел 8.3.1). 3.6.1. Основы перегрузки Нижепривеленный пример иллюстрирует базовый способ применения перегрузки: с1аээ сопр1ех доиЬ1е ге, 1п; рэЬ11с: сопр1ех ЫоиЬ1е); сопр1ех(г)оэЬ1е,боеЬ1е); ИИИИИИИВ Ромсдение С++ (г1еог( сопр1ех орегасог+(сопр1ех, сопр1ех); Гг1еог) сопр1ех орегасог*(сопр1ех, сопр1ех); ): С помощью этого класса простые выражения с комплексными числами преобразуются в вызовы функций: но(с) 1(сопр1ех г1, сопр1ех г2) ( сопр1ех г3 = г1»х2; // орегасог+(г1,г2) По умолчанию присваивание и инициализацию определены как почленное копирование (см.
раздел 11А.1). Проектируя механизм перегрузки, я полагался на преобразования, чтобы уменьшить число перегружаемых функций. Например: ногг( о(сопр1ех г1, сопр1ех г2, г)оиЫе о) ( сопр1ех з3 = г1»г2; // орегагог»(г1,г2) сопр1ех г4 = х1»о; // орегасог+(г1,сопр1ех((() ) сопр1ех з5 = г)»г2) // орегагог+(сопр1ех(й),г2) ) с1азз сопр1ех ( риЫ1с: гг1еос) сопр1ех орегасог»(сопр1ех, сопр1ех); гг1ево сопр1ех орегасог»(сопр1ех, г)оиЫе); гг1епо сопр1ех орегасог»(с)оиые, сопр1ех); // )' Язык без неявных преобразований стал бы проще, не было бы места злоупотреблениям ими. К тому же вызов с использованием неявного преобразования обычно менее эффективен, чем вызов функции с точно соответствующими типами аргументов. Рассмотрим четыре базовых арифметических операции.
Чтобы определить полный набор смешанных операций над числами типа сопр1ех и с(оиЬ1е, требуется 12 арифметических функций. И всего 4 функции плюс один конвертор, если использовать неявные преобразования. Если же число операций и типов больше, то разница между линейным ростом числа функций в случае применения Иными словами, неявное преобразование с(оиЬ1е в сопр1ех позволяет поддержать «смешанную арифметику» с помощью одной функции сложения комплексных чисел. Для повышения эффективности и точности вычислений можно было бы ввести дополнительные функции.
Мы могли вообще обойтись без неявных преобразований, если бы потребовали, чтобы преобразование всегда выполнялось явно, или предоставили полный набор функций сложения: Перегрузка 11ИИИИИШ преобразований и квадратичным, если обходиться без них, становится весьма ощутимой. Я знаю примеры, где приходилось писать полный набор операторов, поскольку не удавалось безопасно определить конверторы. В результате получалось более 100 функций.
Возможно, в особых случаях это и приемлемо, но в качестве постоянной практики вряд ли. Естественно, не все конструкторы определяют разумное и предсказуемое преобразование. Например, у типа чес сог обычно есть конструктор, которому передается целый аргумент, обозначающий число элементов. Если бы предложение я=7 конструировало вектор из семи элементов и присваивало его ч, это следовало бы счесть нежелательным побочным эффектом. Я не считал проблему первостепенной. Однако несколько членов комитета по стандартизации С++, в первую очередь Натан Майерс ()час))ап Муегз), настаивали на необходимости срочного решения.
Оно было найдено уже в 1995 гл в объявление конструктора разрешили включать префикс ехр11ссс. Конструктор, объявленный как ехр11с(с, используется только для явного конструирования, но не для неявных преобразований. Например, если в объявлении класса чесгог имеется конструктор ехр11с(С нессог (1пс );, то предложение ч=7 вызовет ошибку компиляции, тогда как бо- ЛЕЕ ЯВНОЕ Зп=ЧЕССОГ(7) Прнзсдст К НОрМаЛЬНОМу КОНСтруИрОВаНИЮ ВЕКтОра и присваиванию его ч. 3.6.2. Функции-члены и дружественные функции Обратите внимание на то, что вместо функции-члена используется глобальная дружественная (г гсепс1) функция оре са сох+.
Это обеспечивает симметричность операндов для операции +. При использовании функций-членов было бы необходимо разрешать имена: чо1о й(соппр1ех п1, соппр1ех п2, п(ои)п1е а) ( соппр1ех аЗ = п1пг2; // а1.орегасог+(х2) соппр1ех а4 = п1+ГМ // г1.орегасог+(соар1ех(д) ) соппр1ех г5 = и(+а2; // п(.орегасог+(а2) ) Тогда потребовалось бы определить, как соединяются объекты типа согпр1ех с объектами встроенного типа п)ои)з1е.
Для этого понадобилось бы увеличить число функций и модифицировать код в разных местах (в определении класса сопр1ех и встроенного типа п)оц)э1е). Вариант был сочтен нежелательным. Я думал о том, чтобы разрешить определение дополнительных операций над встроенными типами, но отверг эту идею, поскольку не хотел нарушать правило, гласящее, что после определения любого типа — встроенного или определенного пользователем — добавление операций с ним уже невозможно.
Были и другие причины: (2 определение преобразований между встроенными типами и так достаточно запутано, чтобы добавлять еще что-либо; а обеспечение смешанной арифметики с помощью функций-членов менее прозрачно, чем решение с помощью сочетания глобальной функции и конвертора. ИИИИИШ!1 Рождение С++ с1авв Ясттпд ( О роЫ1с: Яст1пд(сопят спас*); Ясг1пда оретасот=(сопвс Яст1пда)) ясттпда оретасогь=(сопвс ясг1пда); // // добавить в конец чотб 1(ЯСттпда в1, Ятт1пда в2) ( я1= я2; я1 = "авс)1*; // правильно: в1.орегагог=(ЯСгтпд("авбб")); "аяЖ" = я2; // ошибка: Яст1пд присваивается спас* ) Позже Эндрю Кениг заметил, что операторы присваивания, такие как +=, более фундаментальны и эффективны, чем арифметические операторы вроде +.
Часто лучше определить только функции типа += и *= в виде членов, а обычные операторы типа + и * добавить позже как глобальные функции: ясг1пда яст1пд::орегасог+=(сопят ясгьпда в) ( // добавить я в конец *с)11я гесцгп *с)т1я; ) ягг1пд орегаСоге(сопят ятг1пда в1, сопвг ятт1пда я2) Ясгтпд яьш = я1; всш+=я2; те ьгп явш; Заметим, что модификатор бг1епс1 здесь не нужен и что определение бинарного оператора тривиально.
Для реализации вызова += вообще не требуются временные переменные, а локальная переменная вц)п в реализации + — это единственный временный объект, с которым сталкивается пользователь. Об остальном позаботится компилятор (см. раздел 3.6.4).
Первоначально я предполагал, что любой оператор можно будет реализовать хоть в виде функции-члена, хоть в виде глобальной функции. Удобно, когда простые операторы доступа уже предоставлены в виде функций-членов, а пользователь может определять собственные операторы как глобальные функции. Применительно Использование глобальной функции позволяет определить операторы таким образом, что их аргументы логически эквивалентны. Наоборот, определение оператора в виде функции-члена гарантирует, что для первого (левого) операнда никакие преобразования вызываться не будут. Таким образом, правила имеют зеркальное отражение для операторов, где левый операнд — 1ча1це: 1НИИИИКП Перегрузка к операторам вроде + и — рассуждения были верны, ио в отношении оператора = мы столкнулись с некоторыми проблемами.
Поэтому в версии 2.0 требовалось, чтобы оператор = был членом. Такое изменение было несовместимо с предыдущими версиями, и несколько программ перестало работать. Одним словом, решение далось нам нелегко. Ведь если опера гор = не был членом, то в программе он мог по-разному интерпретироваться в зависимости от места в исходном коде. Вот пример: с1азз Х ( // нет оператора = ); то[6 т(Х а, х Ь) ( а=Ь; ) // предопределенная семантика = хоьд орегасог=(ха,х)/ // запренено в З.э уо[б д(Х а, Х Ь) ( а =Ь; // определенная пользователем семантика 3.6З. Операторные функции Решив поддержать неявные преобразования и, как следствие, модель смешанных операций, я должен был корректно определить такие преобразования.
Один нз возможных механизмов дают конструкторы с единственным аргументом. Имея объявление с1авз сомр1ех ( // сомр1ех(доиЬ1е); // преобразует допбте в сомр1ех // ); можно явно или неявно преобразовать с)опЬ1е в со/ар1ех. Однако при этом проектировщик класса определит преобразование только к этому классу. Довольно часто возникает необходимость создать новый класс, который вписывается Это могло послужить причиной серьезных недоразумений, особенно если оба присваивания находились в разных исходных файлах. Поскольку для оператора += предопределенной семантики класса не существует, то проблем здесь и нс возникает. Однако даже при первоначальном проектировании С++ я предусмотрел следующее ограничение: операторы [), ( ) и -> должны быть членами.
Мне казалось, что опо безобидно и в то же время исключает возможность некоторых ошибок, связанных с тем, что указанные операторы всегда зависят от состояния левого операнда и, как правило, модифицируют его. Впрочем, тут я проявил чрезмерную заботу о пользователях. Рождение С++ ИИИИИИИВ в уже имеющийся контекст. Например, в библиотеке С существует множество функций, принимающих строковые аргументы типа сЬаг*. Когда Джонатан Шопиро впервые разрабатывал полнофункциональный класс Яс гспд, он обнаружил, что ему либо придется продублировать все функции следующего вида: 1пс всг1еп(сопвс спаг"); // исходная С-функция гпс всг1еп(сопвс Ясг1пда); // новая С+а-функция либо предоставить преобразование из Ясгьпд в сопвс сЬаг*.