Реализация класса Shared_ptr

На предыдущей лекции была задана домашняя работа, в которой требовалось реализовать один из видов «умных» указателей – считающий (shared_ptr). Для этого требуется завести два класса: Shared_ptr и Storage. Мы знаем, что в большинстве случаев, согласно правилам инкапсуляции, поля классов следует делать private.
Как же можно организовать у методов класса Shared_ptr возможность доступа к полям класса Storage?

1. Public-методы

Первый вариант - создать внутри Storage public-методы, возвращающие значения private-полей. Однако в этом случае любой пользователь Shared_ptr сможет добраться до данных, хранящихся в нашем вспомогательном классе, что нехорошо. Соответственно ни конструктор, ни методы, ни любые другие члены класса Storage не моогут быть public. Так же не должно быть отдельного хедера, чтобы пользователь Shared_ptr не знал о существовании данного private класса Storage.

2. Дружественный класс

Есть и другой вариант решения возникшей проблемы. В С++ существует возможность разрешить доступ к закрытым членам класса Storage. Для этого нужно объявить класс Shared_ptr его «другом», используя ключевое слово friend.

class Storage {
private:
	int myCounter;
	Object *myPointer;
private:
  	Storage();

friend class Shared_ptr;
};
Теперь из класса Shared_ptr можно обращаться к любым private-членам класса Storage.

Объявление класса

Так как оба класса находятся в одном хедере, то возникает следующая проблема. С одной стороны Shared_ptr использует поля и методы Storage:

class Shared_ptr{
  ... 
  //обращение к Storage
};

С другой стороны в Storage упоминается о существовании Shared_ptr:

class Storage {
  ...
  friend class Shared_ptr;
};

Поэтому необходимо объявить класс Storage вначале заголовочного файла «Shared_ptr.h», просто дописав строчку:
class Storage;
Тогда внутри Shared_ptr мы сможем использовать поля уже объявленного класса Storage.

Ключевое слово friend

Ключевое слово friend можно использовать не только для объявления одного класса «другом» другого. Так же существуют отдельные «дружественные» методы. Например, мы могли написать следующее:

friend Shared_ptr::Shared_ptr(Object *p);

Следует отметить, что при таком использовании ключевого слова friend, в случае необходимости что-то поменять в одном методе, нам придется во второй так же вносить изменения. Это, безусловно, неудобно и требует дополнительной внимательности.

3. Спрятанный класс

Класс Storage теперь закрыт от посторонних вмешательств. Однако при подключении хедера «Shared_ptr.h» пользователь не сможет использовать слово Storage, так как оно уже занято.
Для того чтобы не загромождать пространство имен, предлагается поместить класс Storage внутрь класса Shared_ptr.

class Shared_ptr {
private:
	class Storage {
		... //члены класса Storage
		friend class Shared_ptr;
	};
	
	... //члены класса Shared_ptr
};

Теперь члены класса Storage будут называться по-другому. Например, конструктор выглядит следующим образом:

Shared_ptr::Storage::Storage(){
  ...
} 

Стандартная библиотека (продолжение)

Как мы уже знаем, в стандартной библиотеке существует множество полезных классов. Например, std::string и std::list. Для того чтобы ими пользоваться, необходимо подключить соответствующие заголовочные файлы:

  #include <string> 
  #include <list>
Обратим внимание на то, что названия хедеров не содержат расширения «.h» (в С например существует «string.h», но этот файл совершенно не связан с std::string).
Представим ситуацию, когда в программе хочется использовать функцию printf и контейнер std::list одновременно. Для этого необходимо подключить следующие заголовочные файлы:
  #include <stdio.h>
  #include <list>
В целях повышения у написанного кода современности и эстетичности, в С++ существует хедер <cstdio>. Работает он также как аналогичный <stdio.h> в С.
  #include <сstdio>
  #include <list>

std::list и итераторы

В стандартной библиотеке, как уже говорилось ранее, существуют контейнеры std::list и std::vector. С одной стороны они чем-то похожи, например, у обоих классов существуют аналогичные методы push_back, pop_front и другие. С другой стороны, для std::vector возможно прямое обращение к i-му элементу (vector[i]), а для std::list никакого похожего метода нет.
Таким образом мы умеем заполнять список:

std::list <int> l;
l.push_back(1);
l.push_back(2);
l.push_back(5);
Но что же можно сделать, чтобы, например, вывести все элементы списка?
Для этих целей существует итератор - специальный объект класса, поддерживающий абстракцию указательного типа. То есть с помощью итератора можно будет «пробежаться» по элементам списка (аналогично указателем мы проходили по элементам массива). Рассмотрим работу итератора на примере.
// вывод содержимого списка на экран
for (std::list<int>::iterator it = l.begin(); it != l.end(); ++it) {
  	printf(“%d\n”, *it);
} 

В классе std::list есть вложенный подкласс iterator, для которого перегружены operator + и operator *. Таким образом, операция ++it передвигает итератор на следующий элемент списка, а *it – возвращает то, на что указывает итератор.

Куда указывают итераторы?

Методы l.begin() и l.end() возвращают итератор, указывающий на начало и конец списка соответственно. Так как адрес данного элемента есть адрес точки начала памяти, отведенной под него, то фактически итератор указывает на место между элементами.

список

*l.begin() – это содержимое первого элемента.
++ l.begin() указывает на следующий после первого элемент списка, то есть на второй.

Следует заметить, что операция *l.end() – осмысленного значения не имеет, так как пытается обратиться к содержимому еще несуществующего элемента.

++it или it++?

Рассмотрим что происходит при пред- и постинкрементации:

++it - возвращает *this, предварительно передвинув итератор на 1.
it++ - перемещается к следующему объекту в списке и возвращает объект, который был текущим перед перемещением.

То есть копируется старое значение итератора, производится инкрементация, возвращается запомненное значение. Получается, что перегруженный оператор it++ работает дольше, чем ++it.

Вывод: по возможности использовать более быструю прединкрементацию ++it.

std::vector и итераторы

Как говорилось ранее, std::list и std::vector чем-то похожи. Так, у класса вектор тоже есть подкласс iterator, с которым можно работать аналогичным образом.

std::vector <int> v;
for (std::vector<int>::iterator it = v.begin(); it != v.end(); ++it) {
	printf(“%d\n”, *it);
}
Может оказаться полезным воспользоваться спецификатором typedef, и упростить объявление новых переменных типа std::vector<int>.
typedef std::vector<int> intVector;
intVector v;
for (intVector::iterator it = v.begin(); it != v.end(); ++it) {
	printf(“%d\n”, *it);
}

Теперь для перехода от структуры vector к list нужно поменять лишь строчку typedef-объявления.

Преимущества использования итераторов

1) Необязательно думать, какую структуру использовать.
2) Можем перебирать элементы структуры циклом for.
3) Можем вставлять элемент a на место, куда указывает итератор it, методом l.insert(it, a).
4) Удалять элемент, на который указывает итератор it, методом l.erase(it).

Некорректный итератор

Как мы знаем структура vector реализована с помощью расширяющегося массива. Представим себе ситуацию, когда имеющийся массив из 10 элементов целиком использован, и нам необходимо добавить в него еще один элемент.

v.insert(it, 11);
Программа, для увеличения размера вектора, скорее всего организует новый массив в другой части памяти. Так как итератору никак эта информация не передается, то он по-прежнему будет указывать на старый массив. Получается, что в процессе добавления нового элемента итератор может стать некорректным. А значит необходимо каждый раз обновлять значение итератора, чтобы он указывал на актуальный элемент структуры.
it = v.insert(it, 11);

Различные виды итераторов

Reverse_iterator
При необходимости просмотреть список в обратном порядке, используется так называемый обратный итератор (reverse_iterator).
for (std::list<int>::reverse_iterator it = l.rbegin(); it != l.rend(); ++it) {
	printf(“%d\n”, *it);
}

Операция * всегда возвращает элемент, который стоит перед элементом, на который указывает обратный итератор.
Поэтому эти два выражения эквивалентны: reverse_iterator(v.end()) и v.rbegin().

Отличия от обычного итератора:
Инкрементация (+1) передвигает итератор в обратную сторону
если it = l.end(), то *it теперь имеет смысл - это последний элемент

const_iterator
В ситуации, когда структуру данных изменять нельзя, обычным итератором вроспользоваться не удастся. Для этих целей существует константный итератор.
const std::list<int>;
const_iterator it;

Существуют и другие виды итераторов.

Контейнер set

Работу итераторов мы обсуждали на примере классов std::list и std::vector. Безусловно, итераторы существуют и в любых других коллекциях.

Рассмотрим контейнер set, в котором можно хранить множество - набор уникальных элементов, отсортированных в определенном порядке.

std::set<int> s;
Для множества s например определен метод find. Он возвращает итератор, указывающий на i-й элемент или на s.end(), если искомый не найден.
std::set<int>::iterator it = s.find(20);
if (it != s.end()) {
	//20-й элемент существует
}

Контейнер map

В map хранится множество упорядоченных пар - ключ и значение (Key, Value). Соответственно итератором можно эти пары просматривать.

std::map<std::string, std::string>::iterator  it;
it->first;     // ключ объекта, на который указывает it (Key)
it->second;    // значение объекта (Value)
(*it);         // объект специального типа std::pair<const Key, Value>