Личный опыт разработки ПО

Сборник рецептов

Use of undefined type

комментариев 5

Давайте поговорим о неприятных граблях связанных с удалением неполного типа, что может привести к крайне неприятным последствиям, например милому сердцу Segmentation fault. Связано это с двумя вещами:

  1. Полномочиями данными компилятору автоматически генерировать деструктор, если он не определен в классе
  2. Возможностью вызвать оператор 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. Он не использует проверки, но из-за особенностей его устройства удаление объекта происходит в месте, где уже есть полная информация о его типе.

31st Март 2010
21:47

Рубрика: C++

Метки:

5 комментариев к 'Use of undefined type'

Подписаться на комментарии по RSS или TrackBack.

  1. хорошая статья, спасибо!

    Alek86

    1 апреля 10 22:30

  2. только вот у shаred_ptr, вроде, есть такой минус, что при нем будет автоматически генериться конструктор копирования с не сильно ожидаемым поведением
    так что лучше уж scoped_ptr :)

    Alek86

    1 апреля 10 22:41

  3. Ну как сказать, поведение вполне ожидаемое — увеличение счетчика ссылок на объект, собственно в этом и смысл данного указателя. Даже в названии слово share, что как-бы намекает :)
    А scoped_ptr конечно более прямолинеен, у него конструктор копирования вообще недоступен.

  4. >Откомпилировав приведенный код и
    > выполнив программу вы скорее всего на
    > выводе получите только сообщение из
    > конструктора, деструктор для Test вызван не будет!
    однако:
    http://ideone.com/tDHh23

    Adler

    11 февраля 13 21:40

  5. > однако:
    > http://ideone.com/tDHh23

    Ну если в один файл все положить, то конечно… какой уж тут неполный тип, все как на ладони :)

    Serega.2032

    14 мая 15 17:53

Оставить комментарий