вторник, 26 ноября 2013 г.

C++: Памятка C++, конструкторы, копирование, присвоение, деструкторы

(Скомунизжено с all4study.ru)

Очень долго собирался написать памятку по разным приёмам программирования на C++. Но никак не мог выбрать достаточно компактный способ изложения. Получалось либо непонятно, либо очень длинно. В конце концов, я решил написать небольшой пример, снабжённый комментариями. Здесь, конечно, представлено далеко не всё, что стоило бы включить в памятку, но за-то то, что есть, изложено очень сжато, компактно и, мне кажется, что понятно. Я не планирую останавливаться на этом примере, для других аспектов программирования на C++ я постараюсь придумать другие примеры. Пока же, жду замечаний, предложений и критики этой первой заметки.



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

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

Вспомогательные классы:
• Place — пара координат. Объект этого класса описывает либо точку с координатами (xo, yo), либо прямоугольник (0 <= x < xo, 0 <= y < yo). Для краткости, я не стал вводить два разных класса для описания этих двух сущностей. • PlaceIterator — примитивный итератор, пригодный только для пробегания по всем точкам прямоугольной области. В рамках этого примера никаких других возможностей от итераторов не требуется. • Value — объекты этого класса просто содержат скалярное значение. Основной класс, служащий памяткой, — это шаблон Arena.
• Arena — это контейнер, который ведёт себя как одномерный массив, но индекс у него — векторный объект Place. То есть Arena, фактически, хранит двумерный массив, обращение к которому выглядит как к одномерному. А благодаря итератору PlaceIterator мы можем пробегать по всем элементам массива, не используя вложенных циклов, — тоже, как в одномерном варианте. В коде это широко используется при инициализации и копировании.

Пример-памятка

#include <iostream>

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

// Декларируем класс.
// Это необходимо, так как мы отказались от использования
// заголовочных файлов и полноценных деклараций. Такой
// подход неприемлем в большинстве случаев. Здесь он используется
// только для того, чтобы сделать пример более компактным.
class PlaceIterator;

// Реализация этого класса далека от полноты и совершенства.
// Он сделан таким, чтобы быть максимально компактным и
// понятным, чтобы проиллюстрировать работу объектов класса
// Arena<T>.
class Place {
 private:
 int xx;
 int yy;
 public:
 Place(); // для подстраховки реализацию не делаем (см. ниже)
 Place(int x, int y): xx(x), yy(y) {}
 int x() const { return xx; }
 int y() const { return yy; }
 // Работа с итераторами.
 // Здесь только декларации, описания мы сможем сделать
 // только после описания класса PlaceIterator
 PlaceIterator begin() const;
 PlaceIterator end() const;
};

// Теперь объекты Place можно выводить с помощью iostream
std::ostream & operator<< (std::ostream & os, const Place & p) 
{
 os << "Place(" << p.x() << ", " << p.y() << ")";
 return os;
}

// Строго говоря, этот итератор можно было сделать более
// изящным, воспользовавшись тем, что он сам знает,
// по какой области идёт перебор, а значит, он сам знает,
// когда надо остановиться. Тем не менее, я решил не использовать
// эту возможность и придать итератору более классический
// STL-вид.
// Строго говоря, здесь бы больше подошла не концепция
// итераторов, а концепция интервалов (range)
// (http://www.boostcon.com/site-media/var/sphene/sphwiki/
// attachment/2009/05/08/iterators-must-go.pdf)
// Кроме того, здесь мы стараемся чётко разделять операцию
// инкремента и операцию получения значения. Это хорошая практика.
// Не следует выполнять в одном месте и доступ к внутренним данным
// и изменение состояния объекта (как это обычно происходит в
// методах типа push; такие методы чреваты и неповоротливы).
class PlaceIterator {
private:
 Place curent;
 int width;
public:
 // Уничтожаем возможность создавать итераторы
 // без указания области, по которой будет идти итерация
 PlaceIterator();
 PlaceIterator(const Place & l): curent(0, 0), width(l.x()) {}
 PlaceIterator(const Place & l, const Place & c):
 curent(c), width(l.x()) {}
 // Инкремент должен возвращать ссылку на итератор
 // (-Weffc++)
 // Спорно, но лично я разделяю мнение, высказанное тут
 // http://google-styleguide.googlecode.com
 // /svn/trunk/cppguide.xml#Preincrement_and_Predecrement
 // Пре-инкремент лучше пост-инкремента
 PlaceIterator & operator++() 
 {
  int x = curent.x() + 1;
  int y = curent.y();
  if (x >= width) {
   x = 0;
   ++y;
  }
  curent = Place(x, y);
  return *this;
 }
 // Метод ++ только изменяет внутреннее состояние объекта,
 // метод *, напротив, только извлекает данные, не изменяя
 // состояния. Это упрощает отладку и диагностику; делает
 // работу с объектом более упорядоченной.
 const Place & operator*() const 
 {
  return curent;
 }
 // Для краткости, сделана такая кривоватая реализация
 // сравнения.
 // Хорошей практикой была бы честная реализация operator== и
 // за тем operator!= как !(*this == other)
 bool operator!=(const PlaceIterator & o) const 
 {
  return (curent.x() != o.curent.x() && curent.y() != o.curent.y());
 }
};

// Только после того, как описан класс PlaceIterator мы можем
// описать методы класса Place, связанные с итераторами.
// Пришлось сделать такую корявость, чтобы не разбивать
// пример на несколько файлов.

PlaceIterator Place::begin() const 
{
 return PlaceIterator(*this);
}

PlaceIterator Place::end() const 
{
 return PlaceIterator(*this, *this);
}

// Класс Value сделан просто для подстановки в шаблон
// Arena<T>.
class Value {
private:
 int vv;
 public:
 Value(): vv(-1) {}
 Value(int v): vv(v) {}
 int v() const { return vv; }
};

// Теперь объекты Value можно выводить с помощью iostream
std::ostream & operator<< (std::ostream & os, const Value & v) 
{
 os << "Value(" << v.v() << ")";
 return os;
}

/////////////////////////////////////////////////////////////
//
// Сказочка.
//
// Класс-памятка, иллюстрирующий различные аспекты,
// о которых не надо забывать, при программировании на C++
//
/////////////////////////////////////////////////////////////

template<class T>
class Arena {

 // Шаблон оператора вывода сделан дружественным –
 // распространённый приём.
 template<class U>
 friend
 std::ostream & operator<< (std::ostream & os, const Arena<U> & v);

private:
 // Порядок инициализации переменных в конструкторах
 // будет в точности таким, как настоящий порядок объявлений.
 // (-Wall)
 // Для полной безопасности конструкторов, лучше использовать
 // обёртки для указателей типа auto_ptr. Имеется в виду
 // ситуация, когда исключение обрывает работу конструктора на
 // середине, а деструктор при этом не вызывается. В этом
 // случае уже созданные с помощью new и new[] объекты,
 // не будут уничтожены и получится утечка ресурсов.
 Place size;
 T * values;

public:
 // Так как для объектов этого класса нет смысла в конструкторе
 // без параметров, то мы декларируем этот конструктор, но не
 // создаём для него реализацию. Это приведёт к возникновению
 // ошибок на стадии компиляции, если кто-то попробует создавать
 // элементы этого класса без параметров. Если мы не создадим
 // конструктор без параметров, то компилятор создаст его за нас,
 // а это совсем не то, что нам нужно.
 Arena();
 // Штатный конструктор. Создаёт арену заданных размеров.
 Arena(const Place & p):
  size(p),
  values(new T[p.x()*p.y()]) // для каждого new в деструкторе
 { // должен быть delete
  std::cout << "create Arena at " << this << std::endl;
 }
 // Конструктор копирования нужен почти всегда, когда
 // среди членов класса есть ссылки.
 // (-Weffc++)
 // Если мы создадим конструктор копирования, то компилятор
 // создаст его автоматически.
 Arena<T>(const Arena<T> & a):
  size(a.size),
  values(new T[size.x()*size.y()])
 {
  std::cout << "create copy Arena at " << this <<
  " from " << &a << std::endl;
  for (PlaceIterator i = size.begin(); i != size.end(); ++i) 
  {
   (*this)[*i] = a[*i];
  }
 }
 // Если вы определяете оператор [], то недурственно
 // определить оператор * так, чтобы эти два оператора
 // не конфликтовали. Одним словом, лучше не переопределять
 // оператор [], хотя часто это удобно.
 T & operator[](const Place & p) 
 {
  return values[size.x()*p.y() + p.x()];
 }
 // const-версия нужна обязательно, она используется
 // для const-объектов (см. комментарий в operator=).
 // Но полноценный путь — создание ещё и метода at(i) –
 // аналога const-версии operator[]; это позволит пользователю
 // класса точно указывать метод доступа — const/не-const для
 // любого (const/не-const) объекта.
 const T & operator[](const Place & p) const 
 {
  return values[size.x()*p.y() + p.x()];
 }
 // Оператор присвоения нужен почти всегда, когда среди
 // членов класса есть ссылки.
 // (-Weffc++)
 // Кроме того, оператор присвоения должен всегда возвращать
 // ссылку на *this.
 // (-Weffc++)
 // Компилятор создаст оператор присвоения автоматически, если
 // мы этого не сделаем сами. Это не всегда хорошо.
 Arena<T> & operator=(const Arena<T> & a) 
 {
  if (this == &a) { // обязательно проверяем на
   return *this; // присвоение самому себе
  }
  size = a.size;
  delete [] values;
  values = new T[size.x()*size.y()];
  for (PlaceIterator i = size.begin(); i != size.end(); ++i) 
  {
   // Для *this используется
   // T & operator[](const Place p)
   // так как объект, на который мы получаем ссылку
   // должен быть изменяемым.
   // Для a используется
   // const T & operator[](const Place p) const
   // так как a является const.
   // Скобки вокруг *this необходимы.
   (*this)[*i] = a[*i];
  }
  return *this;
 }
 // Компилятор автоматически создаёт методы взятия адреса
 // объекта и константного объекта (это разные методы).
 // Здесь мы их переопределять не будем, но об этом полезно помнить.
 // Arena<T> * operator&();
 // const Arena<T> * operator&() const;
 // В базовых классах деструкторы должны быть виртуальными
 // (-Weffc++, -Wnon-virtual-dtor)
 ~Arena() 
 {
  std::cout << "delete Arena at " << this << std::endl;
  delete [] values; // при удалении массивов, не забываем "[]"
 }
};

// Довольно топорненькая функция вывода для Arena<T>.
// Благодаря дружественности (см. выше), имеет доступ
// к приватным данным класса Arena<T>.
template<class T>
std::ostream & operator<< (std::ostream & os, const Arena<T> & v) 
{
 os << "Arena at " << &v << " (size=" << v.size << "):";
 for (PlaceIterator i = v.size.begin(); i != v.size.end(); ++i) 
 {
  if ((*i).x() == 0) 
  {
   os << std::endl;
  } // видимыми в коде
  os << "\040" << v[*i]; // пробельные символы полезно делать видимыми
 }
 return os;
}

/////////////////////////////////////////////////////////////
//
// D E M O
//
/////////////////////////////////////////////////////////////

// Пример позволяет убедиться, что объект Arena
// ведёт себя адекватно. Корректно создаётся, удаляется,
// копируется, присваивается, изменяется.
void test() 
{
 Arena<Value> a(Place(2, 2)); // две строки по два элемента
 Arena<Value> b(Place(3, 2)); // две строки по три элемента
 std::cout << "Arena a = " << a << std::endl;
 std::cout << "Arena b = " << b << std::endl;
 b[Place(0, 0)] = 10;
 std::cout << "Arena b = " << b << std::endl;
 a = b;
 std::cout << "Arena a = " << a << std::endl;
 a[Place(1, 0)] = 20;
 Arena<Value> c(b);
 c[Place(2, 0)] = 30;
 std::cout << "Arena a = " << a << std::endl;
 std::cout << "Arena b = " << b << std::endl;
 std::cout << "Arena c = " << c << std::endl;
}

int main() 
{
 // раскомментируйте цикл, чтобы убедиться
 // в отсутствии утечек памяти
 //while (true) {
 std::cout << "Begin test." << std::endl;
 test();
 std::cout << "End test." << std::endl;
 //}
 return 0;
}

Комментариев нет:

Отправить комментарий