|
Самомодификация и переходимость Невидимый ведущий указатель, как и любой умный указатель, может интерпретироваться как переходный тип. Если просто заменить указываемый объект каким-нибудь производным классом, вы фактически изменяете тип всей видимой клиенту комбинации. На этом основано решение проблемы оператора +=, которая требует самомодификации левого операнда, а также возможного оперативного изменения типа «на ходу». Если правый операнд Complex складывается с левым операндом Integer, тип левого операнда приходится менять. // В файле number.h class NBase; // Клиентам об этом ничего знать не нужно class Number { protected: Number(const Number&) {} Number() {} public: virtual NBase& AddTo(const NBase&) = 0; virtual Number& operator+(const Number&) = 0; // И т.д. }; // В файле number.cpp class Integer; class Real; class PNumber : public Number { private: NBase* number; protected: virtual NBase& AddTo(const NBase& n) const { return number->AddTo(n); } // #2 public: PNumber(NBase* n) : number(n) {} virtual Number& operator+(const Number& n) const { number = &(n.AddTo(*number)); // #1 - замена return *this; } }; class NBase : public Number { // Промежуточный базовый класс // Традиционная двойная передача в NBase public: virtual NBase& operator+=(const Integer&) const = 0; virtual NBase& operator+=(const Real&) const = 0; // И т.д. virtual NBase& AddTo(const NBase&) const = 0; virtual Number& operator+(const Number& n) const { return Integer(0); } // Заглушка не вызывается }; class Integer : public NBase { private: int value; protected: virtual NBase& operator+=(const Integer& i) const { if (value + i.value достаточно мало) { value += i.value; return *this; } else { ArbitraryPrecisionInteger api(value); api += i.value; delete this; return api; } public: Integer(int i) : value(i) {} virtual NBase& AddTo(const NBase& n) const { return n + *this; } // #3 }; class Real : public NBase { ... }; Все как и раньше, разве что операторы + превратились в +=, а двойная передача теперь проходит через +=(левый, правый) и AddTo(правый, левый), чтобы мы могли различать два порядка аргументов. Это важно, поскольку в конечном счете мы хотим заменить указываемый объект левого операнда новым. Это происходит в двух местах: 1. Операторная функция PNumber::operator+=(const Number&) автоматически заменяет число полученным новым значением. 2. Операторная функция Integer::operator+=(const Integer&) возвращает управление, если ей не приходится изменять тип; в противном случае после удаления своего объекта она возвращает новый объект другого типа. По вполне понятным причинам я назову вторую из этих функций заменяющей. Заменяющие функции обладают одной экзотической (если не выразиться сильнее) особенностью: нельзя рассчитывать, что адрес объекта перед вызовом остается действительным и после вызова. Разумеется, пользоваться этим обстоятельством можно лишь в том случае, если эту логику удастся запрятать в самую глубокую и темную дыру, чтобы никто в нее не сунулся, но если это удается сделать, хлопотные алгоритмы невероятно упрощаются. Показанный пример надежно работает, пока PNumber действует как ведущий указатель и пока можно гарантировать, что ни один объект, производный от NBase, не будет существовать без ссылающегося на него PNumber. В нашем случае, когда все прячется в файле .cpp, дело обстоит именно так. Для такой простой проблемы программа получилась довольно большой. Я не утверждаю, что ваши хлопоты оправдаются во всех проектах. Мое решение в основном предназначено для ситуаций, в которых вы тратите много времени на разработку иерархии классов многократного использования и можете позволить себе потратить время на повышение модульности. Я привел его, поскольку оно соответствует основной идее книги - выжать из С++ все возможное и невозможное и щедро разбросать головоломки, представляющие интерес даже для самых выдающихся экспертов. |
Copyright 2005. Климов Александр. All Right Reserved.