75
Умные указатели как идиома
Возникающие проблемы стоит разбирать последовательно. До арифметических операций с
указателями мы доберемся позже, поэтому пока будем пользоваться
ptr_diff
.
Оператор ->
Теперь вы знаете, почему оператор
->
был сделан перегружаемым. В полном соответствии с
синтаксисом, описанным в главе 2,
PFoo
теперь обзаводится собственным оператором
->
. Оператора
преобразования хватает для вызова внешних функций. Приведенный ниже вызов функции
f()
работает, потому что у компилятора хватает ума поискать оператор преобразования, соответствующий
сигнатуре функции, и в данном случае оператор
Foo*()
прекрасно подходит.
class PFoo {
private:
Foo*
foo;
public:
PFoo() : foo(NULL) {}
PFoo(Foo* f) : foo(f) {}
operator Foo*() { return foo; }
Foo* operator->() { return foo; }
};
void f(Foo*);
PFoo pf(new Foo);
f(pf);
//
Работает благодаря функции operator Foo*()
pf->MemberOfFoo();
// Работает благодаря функции operator->()
Причина, по которой работает
pf->MemberOfFoo()
, менее очевидна. В левой части оператора
->
указан пользовательский тип, поэтому компилятор ищет перегруженную версию оператора
->
. Он
находит ее, вычисляет и заменяет
pf
возвращаемым значением, которое превращается в новое
левостороннее выражение оператора
->
. Этот процесс рекурсивно продолжается до тех пор, пока
левостороннее выражение не преобразуется к базовому типу. Если таким базовым типом является
указатель на структуру, указатель на класс или указатель на объединение, компилятор обращается к
указанному члену. Если это что-то иное (например,
int
), компилятор злорадно хохочет и выдает
сообщение об ошибке. В нем он оценивает ваш интеллект и перспективы будущей работы на
основании того факта, что вы пытаетесь обратиться к члену чего-то, вообще не имеющего членов. В
любом случае поиск заканчивается при достижении базового типа. Для самых любопытных сообщаю,
что большинство компиляторов, которыми я пользовался, не отслеживает истинной рекурсии вида:
PFoo operator->() { return *this; }
Здесь оператор
->
пользовательского типа возвращает экземпляр этого типа в качестве своего
значения. Компиляторы C++ обычно предпочитают помучить вас в бесконечном цикле.
Итак, у нас появился класс-указатель, который можно использовать везде, где используются указатели
Foo*
: в качестве аргументов функций, слева от оператора
->
или при определении дополнительной
семантики арифметических операций с указателями — всюду, где
Foo*
участвует в сложении или
вычитании.
Параметризованные умные указатели
Один из очевидных подходов к созданию универсальных умных указателей — использование
шаблонов.
template
class SP {
private:
Type*
pointer;
public:
76
SP() : pointer(NULL) {}
SP(Type* p) : pointer(p) {}
operator Type*() { return pointer; }
Type* operator->() { return pointer; }
};
void f(Foo*);
Ptr pf(new Foo);
f(pf);
//
Работает благодаря функции operator Type*()
pf->MemberOfFoo();
// Работает благодаря функции operator->()
Этот шаблон подойдет для любого класса, не только для класса
Foo
. Перед вами — одна из базовых
форм умных указателей. Она используется достаточно широко и даже может преобразовать указатель
на производный класс к указателю на базовый класс при условии, что вы пользуетесь хорошим
компилятором.
Хороший компилятор C++ правильно обрабатывает такие ситуации, руководствуясь следующей
логикой:
1. Существует ли конструктор
P
, который получает
Р<Ваr>
? Нет. Продолжаем поиски.
2. Существует ли в
Р<Ваr>
операторная функция
operator P()
? Нет. Ищем дальше.
3. Существует ли пользовательское преобразование от
Р<Ваr>
к типу, который подходит под
сигнатуру какого-либо конструктора
P
? Да! Операторная функция operator
Bar*()
превращает
Р<Ваr>
в
Bar*
, который может быть преобразован компилятором в
Foo*
.
Фактически выражение вычисляется как
Ptrpf2(Foo*(pb.operator Bar*()))
, где
преобразование
Bar*
в
Foo*
выполняется так же, как для любого другого встроенного
указателя.
Как я уже говорил, все должно работать именно так, но учтите — некоторые компиляторы
обрабатывают эту ситуацию неправильно. Даже в хороших компиляторах результат вложения
подставляемой (
inline
) операторной функции
operator Bar*()
во встроенный
P(Foo*)
может быть совсем не тем, на который вы рассчитывали; многие компиляторы создают вынесенные (а
следовательно, менее эффективные) копии встроенных функций классов вместо того, чтобы
генерировать вложенный код подставляемой функции. Мораль: такой шаблон должен делать то, что
вы хотите, но у компилятора на этот счет может быть другое мнение.
Иерархия умных указателей
Вместо использования шаблонов можно поддерживать параллельные иерархии указателей и объектов,
на которые они указывают. Делать это следует лишь в том случае, если ваш компилятор не
поддерживает шаблоны или плохо написан.
class PVoid {
// Заменяет void*
protected:
void*
addr;
public:
PVoid() : addr(NULL) {}
PVoid(void* a) : addr(a) {}
operator void*() { return addr; }
};
class Foo : public PVoid {
public:
PFoo() : PVoid() {}
PFoo(Foo* p) : PVoid(p) {}
operator Foo*() { return (Foo*)addr; }
Foo* operator->() { return (Foo*)addr; }
Do'stlaringiz bilan baham: |