|
Программирование >> Синтаксис инициирования исключений
private: int value; protected: virtual NBase& operator+(const Integer& i) const { return *(new Integer(va1ue + i.value)); } #4 public: Integer(int i) : value(i) {} virtual NBase& operator+(const NBase& n) const { return n + *this; } #3 class Real : public NBase { ... }; Как и в исходном варианте двойной передачи, постарайтесь не сосредотачивать взгляд и медленно отодвигайте страницу от носа, пока ну ловите суть происходящего. Ниже подробно расписано, что происходит, когда клиент пытается сложить два Number (а на самом деле - два PNumber , но клиент об этом не знает). Предположим, складываются два Integer : 1. Вызывается операторная функция PNumber: :operator+(const Number&) левого указателя. Выражение переворачивается, и вызывается аналогичная функция правого указателя, при этом аргументом является левый указываемый объект. Однако перед тем, как это случается, функция создает PNumber для результата. 2. Вызывается операторная функция PNumber::operator+(const NBase&) левого указателя. Вызов делегируется оператору + указываемого объекта. 3. Вызывается операторная функция Integer::operator+(const NBase&) правого указываемого объекта. Выражение снова переворачивается. 4. Вызывается операторная функция Integer::opeator+(const Integer&) левого указываемого объекта, где наконец и выполняется реальная операция вычисления суммы. В итоге происходит четыре передачи - две для указателей и две для указываемых объектов. Отсюда и название - удвоенная двойная передача. Мы обходимся без преобразований типов, но зато о существовании NBase приходится объявлять на первых страницах газет. Сокращение до трех передач Если мы разрешим программе знать , что изначально слева и справа стоят PNumber, и выполним соответствующее приведение типов, количество передач можно сократить до трех: оставить одну передачу для операторной функции PNumber::operator+(const Number&) плюс две обычные двойные передачи. Первый PNumber приходит к выводу, что справа также стоит PNumber , выполняет понижающее преобразование от Number к PNumber , а затем напрямую обращается к указываемому объекту. При этом удается обойтись без PNumber::operator+(const NBase&). Есть и дополнительное преимущество - при должной осторожности можно удалить из файла .h все ссылки на NBase. Проблема заключается в том, что какой-нибудь идиот может вопреки всем предупреждениям породить от Number свой класс, выходящий за пределы вашей тщательно построенной иерархии. Это будет означать, что не все Number будут обязательно запакованы в PNumber . Только что показанная методика предотвращает создание производных от Number классов за пределами файла .cpp и даже правильно работает с производными классами без оболочек (Number без PNumber ) при условии, что они правильно реализуют схему удвоенной двойной передачи. Как долго результат остается действительным? В показанной выше реализации клиент должен помнить о необходимости избавляться от Number , вызывая delete &aResu1t. Это серьезное ограничение среди прочего усложняет вложенные вычисления, поскольку для всех промежуточных результатов приходится создавать указатель для их последующего удаления. В комитет ANSI поступило предложение (так и не принятое), в соответствии с которым компилятор должен гарантировать, что временная величина в стеке остается действительной до полного вычисления самого большого вмещающего выражения. Если ваш компилятор следует этому правилу, то строку { return *(new Integer(&(va1ue + i.value)); } можно записать в виде { return Integer(va1ue + i.value); } Аналогично создается и PNumber. Возвращаемое значение будет оставаться действительным внутри вычисляемого выражения. Любая ссылка, которая может существовать за пределами вмещающего выражения, должна быть получена вызовом функции makeClone(). Эта функция создает PNumber в куче или присваивает другой Number виртуальным оператором = для невидимых ведущих указателей, о которых говорилось выше. Чтобы ликвидировать эти раздражающие мелкие утечки памяти, можно воспользоваться приемами уплотнения и сборки мусора, рассмотренными в части 4. Самомодификация и переходимость Невидимый ведущий указатель, как и любой умный указатель, может интерпретироваться как переходный тип. Если просто заменить указываемый объект каким-нибудь производным классом, вы фактически изменяете тип всей видимой клиенту комбинации. На этом основано решение проблемы оператора +=, которая требует самомодификации левого операнда, а также возможного оперативного изменения типа на ходу . Если правый операнд 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 Rea1&) 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, дело обстоит именно так.
|
© 2006 - 2024 pmbk.ru. Генерация страницы: 0
При копировании материалов приветствуются ссылки. |