Категории
Самые читаемые
Лучшие книги » Компьютеры и Интернет » Программирование » Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ - Скотт Майерс

Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ - Скотт Майерс

Читать онлайн Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ - Скотт Майерс

Шрифт:

-
+

Интервал:

-
+

Закладка:

Сделать
1 ... 25 26 27 28 29 30 31 32 33 ... 73
Перейти на страницу:

result = oneHalf * 2; // правильно

result = 2 * oneHalf; // ошибка!

Это плохой знак. Умножение должно быть коммутативным (не зависеть от порядка сомножителей), помните?

Источник проблемы становится понятным, если переписать два последних выражения в функциональной форме:

result = oneHalf.operator*(2); // правильно

result = 2.operator*(oneHalf); // ошибка!

Объект oneHalf – это экземпляр класса, включающего в себя operator*, поэтому компилятор вызывает эту функцию. Но с целым числом 2 не ассоциирован никакой класс, а значит, нет для него и функции operator*. Компилятор будет также искать функции operator*, не являющиеся членами класса (в текущем пространстве имен или в глобальной области видимости):

result = operator*(2, oneHalf); // ошибка!

Но в данном случае нет и свободной функции operator*, которая принимала бы аргументы int и Rational, поэтому поиск завершится ничем.

Посмотрим еще раз на успешный вызов. Видите, что второй параметр – целое число 2, хотя Rational::operator* принимает в качестве аргумента объект Rational. Что происходит? Почему 2 работает в одной позиции и не работает в другой?

Происходит неявное преобразование типа. Компилятор знает, что вы передали int, а функция требует Rational, но он также знает, что можно получить подходящий объект, если вызвать конструктор Rational c переданным вами аргументом int. Так он и поступает. Иными словами, компилятор трактует показанный выше вызов, как если бы он был написан примерно так:

const Rational temp(2); // создать временный объект Rational из 2

result = oneHalf * temp; // то же, что oneHalf.operator*(temp);

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

result = oneHalf * 2; // ошибка! (при наличии explicit-конструктора):

// невозможно преобразовать 2 в Ratinal

result = 2 * oneHalf; // та же ошибка, та же проблема

Со смешанной арифметикой при таком подходе придется распроститься, но, по крайней мере, такое поведение непротиворечиво.

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

result = oneHalf * 2; // правильно (при не explicit-конструкторе)

result = 2 * oneHalf; // ошибка! (даже при не explicit-конструкторе)

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

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

class Rational {

... // не содержит operator*

};

const Rational operator*(const Rational& lhs, // теперь свободная функция

const Rational& rhs)

{

return Rational(lhs.numerator() * rhs.numerator(),

lhs.denominator() * rhs.denominator());

}

Rational oneFourth(1, 4);

Rational result;

result = oneFourth * 2; // правильно

result = 2 * oneFourth; // ура, работает!

Это можно было бы назвать счастливым концом, если бы не одно «но». Должен ли operator* быть другом класса Rational?

В данном случае ответом будет «нет», потому что operator* может быть реализован полностью в терминах открытого интерфейса Rational. Приведенный выше код показывает, как это можно сделать. И мы приходим к важному выводу: противоположностью функции-члена является свободная функция, а функция – друг класса. Многие программисты на C++ полагают, что раз функция имеет отношение к классу и не должна быть его членом (например, из-за необходимости преобразовывать типы всех аргументов), то она должна быть другом. Этот пример показывает, что такое предположение неправильно. Если вы можете избежать назначения функции другом класса, то должны так и поступить, потому что, как и в реальной жизни, друзья часто доставляют больше хлопот, чем хотелось бы. Конечно, иногда отношения дружественности оправданы, но факт остается фактом: если функция не должна быть членом, это не означает автоматически, что она должна быть другом.

Сказанное выше правда, и ничего, кроме правды, но это не вся правда. Когда вы переходите от «Объектно-ориентированного C++» к «C++ с шаблонами» (см. правило 1) и превращаете Rational из класса в шаблон класса, то вступают в силу новые факторы, новые способы их учета, и появляются неожиданные проектные решения. Все это является темой правила 46.

Что следует помнить

• Если преобразование типов должно быть применимо ко всем параметрам функции (включая и скрытый параметр this), то функция не должна быть членом класса.

Правило 25: Подумайте о поддержке функции swap, не возбуждающей исключений

swap – интересная функция. Изначально она появилась в библиотеке STL и с тех пор стала, во-первых, основой для написания программ, безопасных в смысле исключений (см. правило 29), а во-вторых, общим механизмом решения задачи и присваивания самому себе (см. правило 11). Раз уж swap настолько полезна, то важно реализовать ее правильно, но рука об руку с особой важностью идут и особые сложности. В этом правиле мы исследуем, что они собой представляют и как с ними бороться.

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

namespace std {

template <typename T> // типичная реализация std::swap

void swap(T& a, T& b) // меняет местами значения a и b

{

T temp(a);

a = b;

b = temp;

}

}

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

Стандартная реализация swap, может быть, не приведет вас в восторг. Она включает копирование трех объектов: a в temp, b в a и temp – в b. Для некоторых типов ни одна из этих операция в действительности не является необходимой. Для таких типов swap по умолчанию – быстрый путь на медленную дорожку.

Среди таких типов сразу стоит выделить те, что состоят в основном из указателей на другой тип, содержащий реальные данные. Общее название для таких проектных решений: «идиома pimpl» (pointer to implementation – указатель на реализацию – см. правило 31). Спроектированный так класс Widget может быть объявлен следующим образом:

class WidgetImpl { // класс для данных Widget

public: // детали несущественны

...

private:

int a,b,c; // возможно, много данных –

std::vector<double> v; // копирование обойдется дорого

...

};

class Widget { // класс, использующий идиому pimpl

public:

Widget(const Widget& rhs);

Widget& operator=(const Widget& rhs) // чтоб скопировать Widget, копируем

{ // его объект WidgetImpl. Детали

... // реализации operator= как такового

*pimpl = *(rhs.pimpl); // см. в правилах 10, 11 и 12

...

}

...

private:

WidgetImpl *pimpl; // указатель на объект с данными

}; // этого Widget

Чтобы обменять значения двух объектов Widget, нужно лишь обменять значениями их указатели pimpl, но алгоритм swap по умолчанию об этом знать не может. Вместо этого он не только трижды выполнит операцию копирования Widget, но еще и три раза скопирует Widgetlmpl. Очень неэффективно!

А нам бы хотелось сообщить функции std::swap, что при обмене объектов Widget нужно обменять значения хранящихся в них указателей pimpl. И такой способ существует: специализировать std::swap для класса Widget. Ниже приведена основная идея, хотя в таком виде код не скомпилируется:

namespace std {

template <> // это специализированная версия

void swap<Widget>(Widget& a, // std::swap, когда T есть

Widget& b) // Widget; не скомпилируется

{

swap(a.pimpl, b.pimpl); // для обмена двух Widget просто

} // обмениваем их указатели pimpl

}

Строка «template <>» в начале функции говорит о том, что это полная специализация шаблона std::swap, а «<Widget>» после имени функции говорит о том, что это специализация для случая, когда T есть Widget. Другими словами, когда общий шаблон swap применяется к Widget, то должна быть использована эта реализация. Вообще-то не допускается изменять содержимое пространства имен std, но разрешено вводить полные специализации стандартных шаблонов (подобных swap) для созданных нами типов (например, Widget). Что мы и делаем.

1 ... 25 26 27 28 29 30 31 32 33 ... 73
Перейти на страницу:
На этой странице вы можете бесплатно скачать Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ - Скотт Майерс торрент бесплатно.
Комментарии