Что такое smart pointer я думаю известно, на всякий случай о них можно прочитать в Википедии. Существуют различные виды умных указателей, и каждый из них имеет свою область применения, но одно у них общее - они обеспечивают управление (в том или ином виде) сроком жизни сырого (raw) указателя.
В данном примере мы рассмотрим три вида smart pointer. Все они предоставляют практические одинаковые возможности по управлению raw pointer. Также мне бы хотелось разобраться почему в новом стандарте C++11 auto_ptr объявлен устаревшим, что нужно использовать вместо него, и какие возможности есть в компиляторах не поддерживающих новый стандарт.
Основная проблема auto_ptr заключается в том, что он позволяет применить оператор delete к неполному (incomplete) типу, но если оператор delete применяется к неполному типу, то в этом случае не вызывается деструктор этого типа.
Реализация деструктора auto_ptr в stdlibc++ от gcc-4.4.5 тривиальна: ~auto_ptr() { delete _M_ptr; }
То есть данная реализация подвержена этой проблеме. Приведу небольшой пример, подтверждающий это:
Реализация не менее тривиальна:
Для наглядности я вывожу сообщение о вызове конструктора и деструктора.
Теперь создадим класс, который содержит в себе умный указатель на SimpleClass: идиома PImpl:
Шаблон auto_ptr позволяет декларировать объект неполного типа, потому-что в себе он содержит только указатель:
Реализация ClassOwner тоже тривиальна:
Теперь сама функция main:
После сборки получаем такой вывод:
SimpleClass::ctor
После этого незначительного изменения программа работает как надо:
SimpleClass::ctor
SimpleClass::dtor
В данном примере мы рассмотрим три вида smart pointer. Все они предоставляют практические одинаковые возможности по управлению raw pointer. Также мне бы хотелось разобраться почему в новом стандарте C++11 auto_ptr объявлен устаревшим, что нужно использовать вместо него, и какие возможности есть в компиляторах не поддерживающих новый стандарт.
Основная проблема auto_ptr заключается в том, что он позволяет применить оператор delete к неполному (incomplete) типу, но если оператор delete применяется к неполному типу, то в этом случае не вызывается деструктор этого типа.
Реализация деструктора auto_ptr в stdlibc++ от gcc-4.4.5 тривиальна: ~auto_ptr() { delete _M_ptr; }
То есть данная реализация подвержена этой проблеме. Приведу небольшой пример, подтверждающий это:
#ifndef SIMPLECLASS_H
#define SIMPLECLASS_H
class SimpleClass
{
public:
SimpleClass();
~SimpleClass();
};
#endif // SIMPLECLASS_H
|
_Winnie C++ Colorizer |
Реализация не менее тривиальна:
#include "SimpleClass.h"
#include <iostream>
SimpleClass::SimpleClass()
{
std::cout << "SimpleClass::ctor" << std::endl;
}
SimpleClass::~SimpleClass()
{
std::cout << "SimpleClass::dtor" << std::endl;
}
|
_Winnie C++ Colorizer |
Для наглядности я вывожу сообщение о вызове конструктора и деструктора.
Теперь создадим класс, который содержит в себе умный указатель на SimpleClass: идиома PImpl:
#ifndef CLASSOWNER_H
#define CLASSOWNER_H
#include <memory>
class ClassOwner
{
public:
ClassOwner();
private:
std::auto_ptr<class SimpleClass> mImpl;
};
#endif // CLASSOWNER_H
|
_Winnie C++ Colorizer |
Шаблон auto_ptr позволяет декларировать объект неполного типа, потому-что в себе он содержит только указатель:
template<typename _Tp>
class auto_ptr
{
private:
_Tp* _M_ptr;
|
_Winnie C++ Colorizer |
Реализация ClassOwner тоже тривиальна:
#include "ClassOwner.h"
#include "SimpleClass.h"
ClassOwner::ClassOwner(): mImpl(new SimpleClass)
{
}
|
_Winnie C++ Colorizer |
Теперь сама функция main:
#include "ClassOwner.h"
int main()
{
ClassOwner o;
return 0;
}
|
_Winnie C++ Colorizer |
После сборки получаем такой вывод:
SimpleClass::ctor
А это немного не то, что мы ожидали увидеть.
Каким образом эту проблему можно обойти - добавить деструктор к классу ClassOwner:
#ifndef CLASSOWNER_H
#define CLASSOWNER_H
#include <memory>
class ClassOwner
{
public:
ClassOwner();
~ClassOwner();
private:
std::auto_ptr<class SimpleClass> mImpl;
};
#endif // CLASSOWNER_H
|
_Winnie C++ Colorizer |
#include "ClassOwner.h"
#include "SimpleClass.h"
ClassOwner::ClassOwner(): mImpl(new SimpleClass)
{
}
ClassOwner::~ClassOwner()
{
/// needed only for correct destruction of auto_ptr
}
|
_Winnie C++ Colorizer |
После этого незначительного изменения программа работает как надо:
SimpleClass::ctor
SimpleClass::dtor
Я думаю не стоит объяснять что данная проблема может приводить к утечкам памяти (ресурсов), странному поведению программы и пр.
Вариантов решения данной проблемы несколько:
- Не использовать auto_ptr - самый радикальный вариант, ниже я приведу примеры полноценной замены. Понятно что данный вариант может быть недоступен, если вы не используете по каким-то причинам сторонние библиотеки (Boost) или у ваша версия компилятора не поддерживает C++11.
- Заставить компилятор выдавать warning или error (однако, для gcc я не смог найти такой ключ, хотя пробовал ключи -Wall -Wextra -pedantic -Weffc++).
- Использовать статические анализаторы кода (PVS studio и пр.).
- Провести code-review на предмет использования auto_ptr
Замена std::auto_ptr на std::unique_ptr
Чтобы использовать данную возможность необходим компилятор с поддержкой стандарта C++11 (бывший C++0x). Я пробовал компилятор g++-4.4 из Debian squeeze. Он имеет частичную поддержку нового стандарта и шаблон unique_ptr в нем уже есть.
Итак, меняем в нашем классе auto_ptr на unique_ptr и пытаемся скомпилировать (добавляем флаг -std=c++0x):
#ifndef CLASSOWNER_H
#define CLASSOWNER_H
#include <memory>
class ClassOwner
{
public:
ClassOwner();
//~ClassOwner();
private:
/// @note compiled with -std=c++0x
std::unique_ptr<class SimpleClass> mImpl;
};
#endif // CLASSOWNER_H
|
_Winnie C++ Colorizer |
Получаем сообщение об ошибке:
usr/include/c++/4.4/bits/unique_ptr.h:64: ошибка: некорректное применение ‘sizeof’ к неполному типу ‘SimpleClass’
/usr/include/c++/4.4/bits/unique_ptr.h:62: ошибка: static assertion failed: "can\'t delete pointer to incomplete type"
Итак, что же произошло: компилятор не смог вычислить sizeof от неполного типа, о чем нам и сообщил. Во второй строке мы видим более понятное сообщение, о том что не можем применить delete к указателю на неполный тип, это как раз то что нам нужно.
Каким образом он этого добился? Посмотрим на декларацию шаблона unique_ptr:
Мы видим, что данный шаблонный класс принимает два параметра: первый - тип данных, который мы оборачиваем, и второй параметр - функтор, который вызовется при выполнении операции reset (в деструкторе, или явный вызов), по умолчанию ставится функтор default_delete, вот его реализация:
Мы видим, что в операторе () делается проверка времени компиляции (static_assert), в котором как раз проверяется, что sizeof от типа положителен, и если это утверждение неверно, то выдается ошибка компиляции с заданным сообщением.
Если мы раскоментируем декларацию и иплементацию деструктора ClassOwner, то компиляция пройдет успешно, и после выполнения программы мы увидим вызов конструктора и деструктора как и ожидали.
На самом деле класс unique_ptr предоставляет более гибкий механизм RAII по сравнению с auto_ptr, как раз благодаря второму параметру в шаблоне, ниже я приведу простой пример как его можно использовать:
В данной функции мы получаем ресурс FILE - сишный способ работы с файлами, и сразу задаем deleter для данного ресурса - в данном случае это закрыть файл. При данном подходе мы получаем все прелести RAII без написания сложных оберток вокруг защищаемых ресурсов.
Замена std::auto_ptr на boost::scoped_ptr
Библиотека Boost предоставляет огромное количество компонент для ускорения разработки на C++, о ней можно долго говорить, но сейчас мы будем рассматривать только ее scoped_ptr.
Подключаем Boost к проекту (я использую CMake):
find_package(Boost REQUIRED)
Пытаемся скомпилировать:
Как мы видим, здесь похожая ситуация: мы опять используем sizeof для того чтобы сформировать ошибку, но основное отличие в том, что мы пытаемся определить новый тип: массив.
Итак, смотрим деструктор scoped_ptr:
Мы видим, что вызывается функция checked_delete, вот ее реализация:
Итак, что здесь происходит:
В первой строке мы пытаемся объявить тип type_must_be_complete - это массив. Если sizeof(T) != 0, то массив будет длины 1, в противном случае -1. Здесь используется две особенности языка C++:
Две указанные особенности используются для того чтобы убедиться, что тип T полностью определен в момент инстанцирования шаблонной функции checked_delete, то есть при вызове деструктора scoped_ptr.
Итог:
Проблема auto_ptr стала понятна, но от старого кода (компилятора) никуда не деться, поэтому в уже существующих проектах можно просмотреть все места использования auto_ptr и проверить, что деструктор правильно вызовется. Также рекомендуется использовать уже готовые компоненты, типа Boost, Qt (есть класс QScopedPointer, который похож на std::unique_ptr), наконец, перейти на использование C++11, благо, что сейчас современные компиляторы уже имеют подержку нового стандарта. Также стал понятен механизм проверки, что тип является полностью определенным (complete type). Вкратце, макрос BOOST_STATIC_ASSERT аналогичным образом (но чуть-чуть сложнее :)) предоставляет механизм проверки утверждений на этапе компиляции.
Каким образом он этого добился? Посмотрим на декларацию шаблона unique_ptr:
template <typename _Tp, typename _Tp_Deleter = default_delete<_Tp> >
class unique_ptr
|
_Winnie C++ Colorizer |
Мы видим, что данный шаблонный класс принимает два параметра: первый - тип данных, который мы оборачиваем, и второй параметр - функтор, который вызовется при выполнении операции reset (в деструкторе, или явный вызов), по умолчанию ставится функтор default_delete, вот его реализация:
/// Primary template, default_delete.
template<typename _Tp>
struct default_delete
{
default_delete() { }
template<typename _Up>
default_delete(const default_delete<_Up>&) { }
void
operator()(_Tp* __ptr) const
{
static_assert(sizeof(_Tp)>0,
"can't delete pointer to incomplete type");
delete __ptr;
}
};
|
_Winnie C++ Colorizer |
Если мы раскоментируем декларацию и иплементацию деструктора ClassOwner, то компиляция пройдет успешно, и после выполнения программы мы увидим вызов конструктора и деструктора как и ожидали.
На самом деле класс unique_ptr предоставляет более гибкий механизм RAII по сравнению с auto_ptr, как раз благодаря второму параметру в шаблоне, ниже я приведу простой пример как его можно использовать:
#include <memory>
#include <cstdio>
#include <iostream>
struct stdio_deleter
{
void operator ()(FILE * handle)
{
if (handle)
{
fclose(handle);
std::cout << "closed" << std::endl;
}
}
};
void workWithStdioBasedFiles()
{
typedef std::unique_ptr<FILE, stdio_deleter> file_ptr;
file_ptr handle(fopen("file.txt", "w"));
/**
* work with file, we also may throw exception
*/
}
|
_Winnie C++ Colorizer |
Замена std::auto_ptr на boost::scoped_ptr
Библиотека Boost предоставляет огромное количество компонент для ускорения разработки на C++, о ней можно долго говорить, но сейчас мы будем рассматривать только ее scoped_ptr.
Подключаем Boost к проекту (я использую CMake):
find_package(Boost REQUIRED)
include_directories(${Boost_INCLUDE_DIR})Убираем ключ -std=c++0x, меняем реализацию класса:
#ifndef CLASSOWNER_H
#define CLASSOWNER_H
#include <boost/smart_ptr/scoped_ptr.hpp>
class ClassOwner
{
public:
ClassOwner();
//~ClassOwner();
private:
boost::scoped_ptr<class SimpleClass> mImpl;
};
#endif // CLASSOWNER_H
|
_Winnie C++ Colorizer |
Пытаемся скомпилировать:
/usr/include/boost/checked_delete.hpp:32: ошибка: некорректное применение ‘sizeof’ к неполному типу ‘SimpleClass’
/usr/include/boost/checked_delete.hpp:32: ошибка: creating array with negative size (‘-0x00000000000000001’)
Как мы видим, здесь похожая ситуация: мы опять используем sizeof для того чтобы сформировать ошибку, но основное отличие в том, что мы пытаемся определить новый тип: массив.
Итак, смотрим деструктор scoped_ptr:
template<class T> class scoped_ptr // noncopyable
{
private:
T * px;
/// ...
~scoped_ptr() // never throws
{
#if defined(BOOST_SP_ENABLE_DEBUG_HOOKS)
boost::sp_scalar_destructor_hook( px );
#endif
boost::checked_delete( px );
}
|
_Winnie C++ Colorizer |
// verify that types are complete for increased safety
template<class T> inline void checked_delete(T * x)
{
// intentionally complex - simplification causes regressions
typedef char type_must_be_complete[ sizeof(T)? 1: -1 ];
(void) sizeof(type_must_be_complete);
delete x;
}
|
_Winnie C++ Colorizer |
В первой строке мы пытаемся объявить тип type_must_be_complete - это массив. Если sizeof(T) != 0, то массив будет длины 1, в противном случае -1. Здесь используется две особенности языка C++:
- sizeof от неполного типа приводит к ошибке компиляции
- Нельзя объявить массив отрицательной длины
Две указанные особенности используются для того чтобы убедиться, что тип T полностью определен в момент инстанцирования шаблонной функции checked_delete, то есть при вызове деструктора scoped_ptr.
Итог:
Проблема auto_ptr стала понятна, но от старого кода (компилятора) никуда не деться, поэтому в уже существующих проектах можно просмотреть все места использования auto_ptr и проверить, что деструктор правильно вызовется. Также рекомендуется использовать уже готовые компоненты, типа Boost, Qt (есть класс QScopedPointer, который похож на std::unique_ptr), наконец, перейти на использование C++11, благо, что сейчас современные компиляторы уже имеют подержку нового стандарта. Также стал понятен механизм проверки, что тип является полностью определенным (complete type). Вкратце, макрос BOOST_STATIC_ASSERT аналогичным образом (но чуть-чуть сложнее :)) предоставляет механизм проверки утверждений на этапе компиляции.
Что-то я сколько ни пробовал заставить "не вызваться" деструктор auto_ptr'а из первого примера - ничего не получается. Да собственно, я и понять не могу, как такое может выйти - у класса ClassOwner генерится деструктор по-умолчанию. Иначе и быть не может. Соотв. дёргается деструктор auto_ptr и т.д.
ОтветитьУдалитьПроблема auto_ptr в другом - у него копирующий конструктор совсем не копирующий: сигнатура auto_ptr(auto_ptr& x). Это значит, что он изменяет экземпляр, из которого копирует. Т.е. это и не копирование совсем. А вот uniq_ptr решает эту проблему и кучку других.
Да, вы безусловно правы, что у ClassOwner генерируется деструктор по умолчанию. Но речь идет не про ClassOwner, а про SimpleClass, у которого определен пользовательский деструктор (в котором мы выводим сообщение на консоль). Здесь особенность заключается в том, что C++ (да и C) позволяет работать с неполными (incomplete) типами. И в случае C++ при удалении указателя на неполный тип компилятор не вызовет для него деструктор, так как он, грубо говоря, не знает где его искать: у компилятора есть только имя типа, но нет никакой информации о том как уничтожить объект. Ведь эта информация может даже не быть в полученном бинарнике (пример - сборка статической библиотеки).
УдалитьЧто касается примера, то он полностью верен, я его проверил на g++-4.4.5. также выложил полный пример: github.com/prograholic/blog/tree/master/auto_ptr_tests
Да, вы правы, я ошибся с примером. :)
УдалитьНо всё-таки главное отличие uniq_ptr от auto_ptr, то, что он не копируемый, а _перемещаемый_. Т.е. вы не можете сделать вот так:
std::uniq_ptr p(new int);
std::uniq_ptr p2 = p;
а можете только:
std::unique_ptr p(new int);
std::unique_ptr p2 = std::move(p);
Это значит, что умный указатель p может передаваться, как rvalue без проблем.
И ещё unique_ptr умеет корректно удалять массивы объектов, т.к. использует оператор delete[], а не просто delete.
Мне кажется товарисч Аноним не допонял всей эпичности данной ситуации
ОтветитьУдалитьstruct X;
func(X *px)
{
delete px; // компилятор проглотит, ошибки не будет, и деструктор не будет вызван, даже если он есть он где то есть
}
Да, все верно
УдалитьЗабыты и не указаны главные вещи для программера:
ОтветитьУдалить1) Алгоритмические сложности популярных операций с умными указателями
2) Насколько увеличится размер исполняемого файла(в Байтах)
1 - А какие тут могут быть алгоритмические сложности? вызов inline функции, плюс опционально разыменование указателя. В теории грамотный компилятор все это уберет.
Удалить2 - Абсолютно не главная вещь, только если вы не embedded программист.
>>2 - Абсолютно не главная вещь, только если вы не embedded программист.
УдалитьМы с Вами живем в эпоху глобального погружения йафоны\йапады и др. девайсы! ;) Так что надо знать!
>>вызов inline функции, плюс опционально разыменование указателя.
А при чему тут inline? Вы осознаете термин "алгоритмическая сложность" ? Любой алгоритм имеет сложность и совсем не важно будет ли он вставлен по месту вызова или нет.
O(1), к гадалке не ходи
Удалить