Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Программирование на C / C++ / C++ for real programmers.pdf
Скачиваний:
234
Добавлен:
02.05.2014
Размер:
2.04 Mб
Скачать

184

virtual Foo* makeClone() { return new PFoo(foo->makeClone()); }

};

class Bar : public Foo { protected:

Bar(Bar&); // Конструктор копий public:

virtual Foo* makeClone();

};

Foo* Bar::makeClone()

{

return new Bar(*this);

}

Наконец мы добрались и до применения настоящего конструктора копий. Указатель создает копию — не только свою, но и указываемого объекта. В свою очередь, указываемый объект перекладывает всю тяжелую работу на свой собственный конструктор копий. Обратите внимание: чтобы это стало возможным, мы сделали конструктор копий Foo защищенным.

Присваивание

Присваивание для невидимых ведущих указателей похоже на присваивание для любых других типов ведущих указателей. И снова мы продолжаем с того места, на котором остановились при присваивании неведущих указателей.

class Foo { public:

virtual Foo& operator=(const Foo&);

};

// В файле foo.cpp

class PFoo : public Foo { private:

Foo* foo; public:

virtual Foo& operator=(const Foo& f)

{

if (this == &f) return *this; delete foo;

foo = f.foo->makeClone(); return *this;

}

};

Foo& Foo::operator=(const Foo&)

{

return *this;

}

Снова о двойной передаче

Невидимые указатели позволяют элегантно решить многие проблемы. Одна из таких проблем — улучшенная инкапсуляция двойной передачи, и в том числе решение неприятной проблемы, связанной с оператором +=. Мы объединим двойную передачу с концепцией переходных типов, о которых говорилось давным-давно, в главе 7.

185

Удвоенная двойная передача

Итак, давайте попробуем реализовать двойную передачу для невидимых указателей. Этот раздел представляет собой элементарное распространение приемов, которыми мы пользовались без связи с указателями.

Первая попытка

Сейчас мы сделаем первый заход на арифметические операции с невидимыми указателями. Он работает, но обладает некоторыми ограничениями, на которые следует обратить внимание и должным образом исправить. Чтобы избежать проблем, связанных с возвращением ссылок на временные значения (см. окончание главы 11), я перехожу на использование оператора new. Проблемы сборки мусора будут рассматриваться позже.

// В файле number.h

class NBase; // Клиентам об этом ничего знать не нужно class Number {

protected:

Number(const Number&) {} Number() {}

public:

virtual NBase& operator+(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& operator+(const NBase& n) const { return *number + n; } // #2

public:

PNumber(NBase* n) : number(n) {}

virtual Number& operator+(const Number& n) const

{ return *(new PNumber(&(n + *number))); } // #1

};

class NBase : public Number {

//Промежуточный базовый класс

//Традиционная двойная передача в NBase public:

virtual NBase& operator+(const Integer&) const = 0; virtual NBase& operator+(const Real&) const = 0;

// И т.д.

virtual NBase& operator+(const NBase&) const = 0; virtual Number& operator+(const Number& n) const

{ return Integer(0); }

// Заглушка не вызывается

};

 

class Integer : public NBase {

 

186

private:

int value; protected:

virtual NBase& operator+(const Integer& i) const

{ return *(new Integer(value + 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 &aResult. Это серьезное ограничение среди прочего усложняет вложенные вычисления, поскольку для всех промежуточных результатов приходится создавать указатель для их последующего удаления. В комитет ANSI поступило предложение (так и не принятое), в соответствии с которым компилятор должен гарантировать, что временная величина в стеке остается действительной

187

до полного вычисления самого большого вмещающего выражения. Если ваш компилятор следует этому правилу, то строку

{return *(new Integer(&(value + i.value)); }

можно записать в виде

{return Integer(value + 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 { // Промежуточный базовый класс

188

// Традиционная двойная передача в 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, дело обстоит именно так.