Это заметка о методах, которые C++ создаёт автоматически, даже если вы их не создавали.
Для кого эта заметка? Надеюсь, что она будет интересна начинающим программистам на С++. А опытным программистам позволит лишний раз освежить и систематизировать свои знания.
Итак, если вы написали
то, знайте, что на самом деле вы создали примерно вот такой класс:
Надо помнить, что эти функции создаются всегда, и могут приводить к неожиданным результатам.
Самое неприятное, когда программа работает, но не так как вы хотели. При этом никаких ошибок с точки зрения языка не возникает. Именно к таким неприятностям приводят неявно создаваемые методы.
Рассмотрим класс, который выдаёт сообщения о создании/удалении объектов и поддерживает статический счётчик объектов (для простоты в виде публичного int).
Реализация тривиальна:
Что будет делать такая программа?
Результат может удивить неподготовленного читателя:
Складывается ощущение, что объект был создан только один раз, а удалён — трижды. Счётчик объектов уходит в минус. При этом программа спокойно отрабатывает и нигде не валится.
Как вы понимаете, это произошло потому, что мы не учли ав��оматически созданный конструктор копирования, который только копирует, но ничего не печатает и не корректирует счётчик.
Мы можем исправить эту ситуацию, если сами допишем конструктор копирования
Теперь мы получим абсолютно разумный результат:
Аналогичные засады возникают при присвоении (operator=); но...
… самые, пожалуй, изысканные подвохи могут возникать, если вы реализовали нетривиальный метод получения адреса
но забыли реализовать его двойник, обладающий теми же (или иными?) нетривиальными свойствами:
Пока ваша программа ограничивается не-константными объектами:
всё работает. Это может продолжаться очень долго, все уже позабудут, как устроен объект CC, проникнутся к нему доверием и не буду думать на него при появлении ошибок.
Но рано или поздно появится код:
И метод
Предательски не сработает (про перегрузку по const я уже писал вот тут).
Но хватит, наверно, примеров. Смысл у них у всех примерно один и тот-же. Как же избежать всех описанных неприятностей.
Самый простой способ — создать прототипы всех методов, создаваемых автоматически и не создавать реализации.
Тогда программа просто не слинкуется и вы получите вполне разумное сообщение. Я получил такое:
Ваш компилятор может выразиться чуть-чуть иначе.
Если этот способ кажется вам корявым (у вас есть на то все основания и я с вами солидарен), то можно создать «полноценные» методы, но сделать их приватными.
Тогда вы тоже получите сообщения об ошибках ещё на этапе компиляции.
Согласитесь, что это сообщение выглядит как-то… поприличней.
Ну и третий способ (последний в списке, но не последний по значению) — просто честно реализовать все необходимые методы, не откладывая это дело в долгий ящик :-)
Для кого эта заметка? Надеюсь, что она будет интересна начинающим программистам на С++. А опытным программистам позволит лишний раз освежить и систематизировать свои знания.
Итак, если вы написали
class Empty {};то, знайте, что на самом деле вы создали примерно вот такой класс:
class Empty {
public:
// Конструктор без параметров
Empty();
// Копирующий конструктор
Empty(const Empty &);
// Деструктор
~Empty();
// Оператор присвоения
Empty& operator=(const Empty &);
// Оператор получения адреса
Empty * operator&();
// Оператор получения адреса константного объекта
const Empty * operator&() const;
};Надо помнить, что эти функции создаются всегда, и могут приводить к неожиданным результатам.
К каким это может привести неприятностям?
Самое неприятное, когда программа работает, но не так как вы хотели. При этом никаких ошибок с точки зрения языка не возникает. Именно к таким неприятностям приводят неявно создаваемые методы.
Пример первый: Конструкторы
Рассмотрим класс, который выдаёт сообщения о создании/удалении объектов и поддерживает статический счётчик объектов (для простоты в виде публичного int).
class CC {
public:
CC();
~CC();
static int cnt;
};Реализация тривиальна:
int CC::cnt(0);
CC::CC() { cnt++; cout << "create\n";}
CC::~CC() { cnt--; cout << "destroy\n";}Что будет делать такая программа?
void f(CC o) {}
int main() {
CC o;
cout << " cnt = " << o.cnt << "\n";
f(o);
cout << " cnt = " << o.cnt << "\n";
f(o);
cout << " cnt = " << o.cnt << "\n";
return 0;
}Результат может удивить неподготовленного читателя:
create cnt = 1 destroy cnt = 0 destroy cnt = -1 destroy
Складывается ощущение, что объект был создан только один раз, а удалён — трижды. Счётчик объектов уходит в минус. При этом программа спокойно отрабатывает и нигде не валится.
Как вы понимаете, это произошло потому, что мы не учли ав��оматически созданный конструктор копирования, который только копирует, но ничего не печатает и не корректирует счётчик.
Мы можем исправить эту ситуацию, если сами допишем конструктор копирования
CC::CC(const CC &) {
cnt++; cout << "create (copy)\n";
}Теперь мы получим абсолютно разумный результат:
create cnt = 1 create (copy) destroy cnt = 1 create (copy) destroy cnt = 1 destroy
Аналогичные засады возникают при присвоении (operator=); но...
Пример второй: Получение адреса
… самые, пожалуй, изысканные подвохи могут возникать, если вы реализовали нетривиальный метод получения адреса
CC * operator&();
но забыли реализовать его двойник, обладающий теми же (или иными?) нетривиальными свойствами:
const CC * operator&() const;
Пока ваша программа ограничивается не-константными объектами:
СС o; CC *p; p = &o;
всё работает. Это может продолжаться очень долго, все уже позабудут, как устроен объект CC, проникнутся к нему доверием и не буду думать на него при появлении ошибок.
Но рано или поздно появится код:
CC const o; CC const *q = &o;
И метод
CC * operator&();
Предательски не сработает (про перегрузку по const я уже писал вот тут).
Но хватит, наверно, примеров. Смысл у них у всех примерно один и тот-же. Как же избежать всех описанных неприятностей.
От этих недоразумений очень легко застраховаться!
Самый простой способ — создать прототипы всех методов, создаваемых автоматически и не создавать реализации.
Тогда программа просто не слинкуется и вы получите вполне разумное сообщение. Я получил такое:
/var/tmp//ccGQszLd.o(.text+0x314): In function `main': : undefined reference to `CC::operator&() const'
Ваш компилятор может выразиться чуть-чуть иначе.
Если этот способ кажется вам корявым (у вас есть на то все основания и я с вами солидарен), то можно создать «полноценные» методы, но сделать их приватными.
Тогда вы тоже получите сообщения об ошибках ещё на этапе компиляции.
count2.cpp: In function 'int main()': count2.cpp:22: error: 'const CC* CC::operator&() const' is private count2.cpp:37: error: within this context
Согласитесь, что это сообщение выглядит как-то… поприличней.
Ну и третий способ (последний в списке, но не последний по значению) — просто честно реализовать все необходимые методы, не откладывая это дело в долгий ящик :-)