...
if ((a*b) == (c*d)) {
} else {
}
Догадываетесь, что не так? Выражение ((a*b) == (c*d)) будет
Легче всего найти объяснение такому неприятному поведению, переписать проверку на равенство в эквивалентной функциональной форме:
if(operator==(operator*(a, b), operator*(c, d)))
Заметьте, что когда вызывается operator==, уже присутствуют два активных вызова operator*, каждый из которых будет возвращать ссылку на статический объект Rational внутри operator*. Таким образом, operator== будет сравнивать статический объект Rational, определенный в функции operator*, со значением статического объект Rational внутри той же функции. Было бы удивительно, если бы они не оказались равны всегда.
Этого должно быть достаточно, чтобы убедить вас, что возвращение ссылки из функции, подобной operator*, – пустая трата времени, но я не настолько наивен, чтобы полагаться на везение. Кое-кто в настоящий момент думает: «Хорошо, если недостаточно одного статического объекта, то, может быть, для этого подойдет статический массив…»
Я не снизойду до того, чтобы посвятить такой программе отдельный пример, но вкратце могу пояснить, почему даже возникновение такой идеи должно повергать вас в стыд. Во-первых, вы должны выбрать n – размер массива. Если n слишком мало, у вас может закончиться место для хранения, и вы ничего не выиграете по сравнению с вышеописанной программой. Если же n чересчур велико, вы уменьшаете производительность вашей программы, поскольку каждый объект в массиве конструируется при первом вызове функции. Это будет стоить вам n вызовов конструкторов и n вызовов деструкторов, даже если данная функция вызывается всего один раз. Если процесс повышения производительности программного обеспечения называется оптимизацией, тогда самое верное название происходящему – «пессимизация». И наконец, подумайте о том, как заносить необходимые вам значения в массив объектов и во что это обойдется. Наиболее прямой способ передачи объектов – операция присваивания, но с чем она связана? В общем случае это вызов деструктора (для уничтожения старого значения) плюс вызов конструктора (для копирования нового значения). А ваша цель – избежать вызовов конструктора и деструктора! Так что затея весьма неудачна (нет-нет: применение векторов вместо массивов не улучшит ситуацию).
Правильный способ написания функции заключается в том, что она должна возвращать новый объект. В применении к operator* для класса Rational это означает либо следующий код, либо нечто похожее:
inline const Rational operator*(const Rational& lhs, Rational& rhs)
{
return Rational(lhs.n*rhs.h, lhs.d*rhs.d);
}
Конечно, в этом случае вам придется смириться с издержками на вызов конструктора и деструктора для объектов, возвращаемых operator*, но в глобальном масштабе это небольшая цена за корректное поведение. Притом, вероятно, все не так уж страшно. Подобно всем языкам программирования, C++ позволяет разработчикам компиляторов применить оптимизацию для повышения производительности генерируемого кода, и, как оказывается, в некоторых случаях вызовы конструктора и деструктора возвращаемого operator* значения можно безопасно устранить. Когда компилятор пользуется этой возможностью (а часто он так и поступает), ваша программа продолжает делать то, чего вы от нее хотите, и даже быстрее, чем ожидалось.
Подведем итог: когда вы выбираете между возвратом ссылки и возвращением объекта, ваша задача заключается в том, чтобы все работало правильно. О том, как сделать этот выбор менее накладным, должен заботиться разработчик компилятора.
• Никогда не возвращайте указатель или ссылку на локальный объект, ссылку на объект, распределенный в «куче», либо указатель или ссылку на локальный статический объект, если есть шанс, что понадобится более, чем один экземпляр такого объекта. В правиле 4 приведен пример ситуации, когда возврат ссылки на локальный статический объект имеет смысл, по крайней мере, в однопоточных средах.
Правило 22: Объявляйте данные-члены закрытыми
В этом правиле мы поговорим о том, почему данные-члены не должны быть открытыми (public). Затем мы убедимся, что все аргументы против открытых данных-членов касаются также защищенных (protected). Это приведет нас к выводу, что данные-члены должны быть закрытыми (private), и на этом мы поставим точку.
Итак, открытые данные-члены. Почему нет?
Начнем с синтаксической непротиворечивости (см. также правило 18). Если данные-члены не будут открытыми, то единственный способ для пользователей добраться до объекта – через функции-члены. Если весь открытый интерфейс будет состоять из функций, то пользователям не нужно будет ломать голову, пытаясь вспомнить, где нужно применять скобки, а где – нет, когда он захотят обратиться к члену класса. Они будут ставить скобки, поскольку ничего, кроме функций, не существует. Долой лишнюю головную боль.
Но, может быть, вы не считаете аргумент о непротиворечивости убедительным. Как насчет того факта, что применение функций обеспечивает более тонкую настройку доступа к данным-членам? Если вы сделаете данные-члены открытыми, каждый будет иметь к ним доступ для чтения и записи, но если вы используете функции для получения и установки значения, то сможете запретить доступ вовсе, разрешить только чтение или чтение-запись. Вы даже сможете реализовать доступ только для записи, если захотите:
class AccessLevels {
public:
...
int getReadOnly() const { return readOnly;}
void setReadWrite(int value) { readWrite = value;}
int getReadWrite() { return readWrite;}
void setWriteOnly(int value) { writeOnly = value;}
private:
int noAccess; // нет доступа к этому int
int readOnly; // доступ к этому int только для чтения
int readWrite; // доступ к этому int для чтения и записи
int writeOnly; // доступ к этому int только для записи
};
Такой точный контроль доступа важен, потому что многие данные-члены
Все еще не убедил? Тогда самое время доставать тяжелую артиллерию: инкапсуляция! Если вы реализуете доступ к данным через функции, то позже сможете не хранить, а вычислять данные-члены, и никто из пользователей вашего класса этого не заметит.
Например, предположим, что вы пишете приложение, в котором автоматическое устройство отслеживает скорость проходящих автомобилей. Когда автомобиль проезжает мимо, его скорость вычисляется и заносится в коллекцию данных о скоростях:
class SpeedDataCollection {
...