☰ Оглавление

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

Обзор классов примера

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

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

Вспомогательные классы:

Основной класс, служащий памяткой, — это шаблон Arena.

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

// Версия 0.3.1 от 30-11-2009
// (c) 2009 Alexey V Michurin

#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