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


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

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

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

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

Основной класс, служащий памяткой, — это шаблон Arena<T>.
• Arena<T> — это контейнер, который ведёт себя как одномерный массив, но индекс у него — векторный объект Place. То есть Arena<T>, фактически, хранит двумерный массив, обращение к которому выглядит как к одномерному. А благодаря итератору 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;
}

Компиллировать

c++ -Wall -Wextra -pedantic -Weffc++ file.cpp


Комментарии запрещены.




Статистика