Давайте поговорим о неприятных граблях связанных с удалением неполного типа, что может привести к крайне неприятным последствиям, например милому сердцу Segmentation fault. Связано это с двумя вещами:
- Полномочиями данными компилятору автоматически генерировать деструктор, если он не определен в классе
- Возможностью вызвать оператор delete для объекта тип которого в точке удаления еще не известен
Рассмотрим простой код и посмотрим, что делает компилятор:
test.h
#ifndef TEST_H #define TEST_H class Test { public: Test(); ~Test(); }; #endif//TEST_H |
test.cpp
#include <iostream> #include "test.h" Test::Test() { std::cout << "Test" << std::endl; } Test::~Test() { std::cout << "~Test" << std::endl; } |
trouble.h
#ifndef TROUBLE_H #define TROUBLE_H #include <memory> class Test; class Trouble { public: Trouble(); private: std::auto_ptr<Test> Test_; }; #endif//TROUBLE_H |
trouble.cpp
#include "test.h" #include "trouble.h" Trouble::Trouble() : Test_(new Test()) { } |
main.cpp
#include "trouble.h" int main(int argc, char* argv[]) { Trouble trouble; return 0; } |
Итак, имеется два класса: Test и Trouble, причем в целях ускорения компиляции разработчик решил сделать предварительное объявление класса Test в trouble.h, но не написал деструктор для класса Trouble, а значит компилятор заботливо создаст деструктор сам. Когда он это сделает? Вообще компилятор не мечется по коду, а последовательно его анализирует.
Начнет он с функции main в main.cpp, определит, что создается объект trouble типа Trouble из trouble.h, обнаружит конструктор.
Далее он сделает вывод, что по выходу из main объект trouble должен быть удален, попытается найти деструктор, не найдет его и создаст его сам.
Что будет в этом деструкторе? Естественно удаление членов класса, то есть в данном случае std::auto_ptr<Test>, в деструкторе которого будет соответственно вызван оператор delete для указателя на Test.
Внимательно следим за руками! В данном месте компилятор еще ничего не знает о типе Test, поскольку еще не дошел до test.h и поэтому оператор delete будет применен к неполному типу, что вызовет неопределенное поведение.
Откомпилировав приведенный код и выполнив программу вы скорее всего на выводе получите только сообщение из конструктора, деструктор для Test вызван не будет!
Далее подробности и методы борьбы с данным явлением.
Что делать? Можно сделать неправильный вывод, что предварительное объявление зло и всегда включать все заголовочные файлы в *.h, что приведет к резко возросшему времени компиляции при изменении заголовочных файлов, так как при этом будут перекомпилированы все заголовочные файлы включающие измененный. В больших проектах это может стать настоящей проблемой. Поэтому будем действовать разумно.
Решение №1
Всегда объявлять деструктор в *.h и определять его в *.cpp. В этом случае компиляция деструктора произойдет в том месте, где для типа Test есть уже полная информация.
Решение №2. Boost в помощь
Хотя некоторые компиляторы предупреждают о проблеме в данной ситуации, но имеет смысл подстраховаться. Не используйте std::auto_ptr! Используйте вместо него boost::scoped_ptr, который заставит вас написать деструктор, выдавая ошибки вроде:
C:\libs\boost-1.41.0\boost/checked_delete.hpp(32) : error C2027: use of undefined type 'Test' c:\work\undefined_type\trouble.h(9) : see declaration of 'Test' C:\libs\boost-1.41.0\boost/smart_ptr/scoped_ptr.hpp(80) : see reference to function template instantiation 'void boost::checked_delete<T>(T *)' being compiled with [ T=Test ] C:\libs\boost-1.41.0\boost/smart_ptr/scoped_ptr.hpp(76) : while compiling class template member function 'boost::scoped_ptr<T>::~scoped_ptr(void)' with [ T=Test ] c:\work\undefined_type\trouble.h(18) : see reference to class template instantiation 'boost::scoped_ptr<T>' being compiled with [ T=Test ] C:\libs\boost-1.41.0\boost/checked_delete.hpp(32) : error C2118: negative subscript |
Или:
/usr/include/boost/checked_delete.hpp: In function ‘void boost::checked_delete(T*) [with T = Test]’: /usr/include/boost/smart_ptr/scoped_ptr.hpp:80: instantiated from ‘boost::scoped_ptr<T>::~scoped_ptr() [with T = Test]’ /home/tma/Documents/undefined_type/trouble.h:15: instantiated from here /usr/include/boost/checked_delete.hpp:32: error: invalid application of ‘sizeof’ to incomplete type ‘Test’ /usr/include/boost/checked_delete.hpp:32: error: creating array with negative size (‘-0x000000001’) /usr/include/boost/checked_delete.hpp:33: error: invalid application of ‘sizeof’ to incomplete type ‘Test’ /usr/include/boost/checked_delete.hpp:33: error: creating array with negative size (‘-0x000000001’) |
Причина ошибки – проверка осуществляемая scoped_ptr перед вызовом delete для объекта. Суть проверки в попытке создания массива:
typedef char type_must_be_complete[ sizeof(T)? 1: -1 ]; (void) sizeof(type_must_be_complete); |
В случае неполного типа будет предпринята попытка создания массива длиной –1, что приведет к ошибке компиляции.
Если по каким либо причинам использовать boost::scoped_ptr нельзя, используйте boost::shared_ptr. Он не использует проверки, но из-за особенностей его устройства удаление объекта происходит в месте, где уже есть полная информация о его типе.
хорошая статья, спасибо!
Alek86
1 апреля 10 22:30
только вот у shаred_ptr, вроде, есть такой минус, что при нем будет автоматически генериться конструктор копирования с не сильно ожидаемым поведением
так что лучше уж scoped_ptr :)
Alek86
1 апреля 10 22:41
Ну как сказать, поведение вполне ожидаемое — увеличение счетчика ссылок на объект, собственно в этом и смысл данного указателя. Даже в названии слово share, что как-бы намекает :)
А scoped_ptr конечно более прямолинеен, у него конструктор копирования вообще недоступен.
Максим Тремпольцев
1 апреля 10 23:00
>Откомпилировав приведенный код и
> выполнив программу вы скорее всего на
> выводе получите только сообщение из
> конструктора, деструктор для Test вызван не будет!
однако:
http://ideone.com/tDHh23
Adler
11 февраля 13 21:40
> однако:
> http://ideone.com/tDHh23
Ну если в один файл все положить, то конечно… какой уж тут неполный тип, все как на ладони :)
Serega.2032
14 мая 15 17:53