Это заметка о методах, которые 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
Согласитесь, что это сообщение выглядит как-то… поприличней.
Ну и третий способ (последний в списке, но не последний по значению) — просто честно реализовать все необходимые методы, не откладывая это дело в долгий ящик :-)