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

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 Type> 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<Foo> pf(new Foo);

 

 

f(pf);

// Работает

благодаря функции operator Type*()

pf->MemberOfFoo();

// Работает

благодаря функции operator->()

Этот шаблон подойдет для любого класса, не только для класса Foo. Перед вами — одна из базовых форм умных указателей. Она используется достаточно широко и даже может преобразовать указатель на производный класс к указателю на базовый класс при условии, что вы пользуетесь хорошим компилятором.

Хороший компилятор C++ правильно обрабатывает такие ситуации, руководствуясь следующей логикой:

1. Существует ли конструктор P<Foo>, который получает Р<Ваr>? Нет. Продолжаем поиски.

2.Существует ли в Р<Ваr> операторная функция operator P<Foo>()? Нет. Ищем дальше.

3.Существует ли пользовательское преобразование от Р<Ваr> к типу, который подходит под сигнатуру какого-либо конструктора P<Foo>? Да! Операторная функция operator Bar*() превращает Р<Ваr> в Bar*, который может быть преобразован компилятором в Foo*. Фактически выражение вычисляется как Ptr<Foo>pf2(Foo*(pb.operator Bar*())), где преобразование Bar* в Foo* выполняется так же, как для любого другого встроенного указателя.

Как я уже говорил, все должно работать именно так, но учтите — некоторые компиляторы обрабатывают эту ситуацию неправильно. Даже в хороших компиляторах результат вложения подставляемой (inline) операторной функции operator Bar*() во встроенный P<Foo>(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; }

РВаr::operator->()

77

};

class Pbar : public PFoo { public:

PBar() : PFoo() {} PBar(Bar* p) : PFoo(p) {}

operator Bar*() { return (Bar*)addr; } Bar* operator->() { return (Bar*)addr; }

};

 

pBar pb(new Bar);

 

pFoo pf(pb);

// Работает, потому что PBar является производным от PFoo

pf->MemberOfFoo();

// Работает благодаря PFoo::operator->

Этот вариант будет работать, если вас не огорчают многочисленные копирования/вставки текста и (в зависимости от компилятора) предупреждения о том, что скрывает PFoo::operator->(). Конечно, такое решение не настолько элегантно, как встроенные типы указателей шаблона Ptr.

Арифметические операции с указателями

Ниже показан пример арифметических операторов, обеспечивающих работу операций сложения/вычитания для умных указателей. Для полной, абсолютно совместимой реализации к ним следует добавить операторы ++ и --.

template <class Type> class Ptr {

private:

Type* pointer; public:

Ptr() : pointer(NULL) {} Ptr(Type* p) : pointer(p) {}

operator Type*() { return pointer; }

ptr_diff operator-(Ptr<Type> p) { return pointer – p.pointer; } ptr_diff operator-(void* v) { return ((void*)pointer) – v; }

Ptr<Type> operator-(long index) { return Ptr<Type>(pointer – index); } Ptr<Type> operator-=(long index) { pointer -= index; return *this; } Ptr<Type> operator+(long index) { return Ptr<Type>(pointer + index); } Ptr<Type> operator+=(long index) { pointer += index; return *this; }

};

Важно понимать, чем ptr_diff отличается от целого индекса. При вычитании одного адреса из другого результатом является смещение, как правило, выраженное в байтах. В случае прибавления целого к указателю адрес изменяется на размер объекта, умноженный на целое. Помните: в C++, как и в С, указатель ссылается не на один объект, а на теоретический массив объектов. Индексы в описанных выше перегруженных операторах представляют собой индексы этого теоретического массива, а не количества байт.

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