Уже год в свободное от работы время я пилю что-то вроде смеси Maven и Spring для С++. Важной её частью является самописная система умных указателей. Зачем мне всё это — отдельная тема. В данной статье я хочу коротко рассказать о том, как одна, казалось бы, полезная фича С++ заставила меня усомниться в здравом смысле Стандарта.

Редактировано:
Приношу свои извинения хабрасообществу и Стандарту. Буквально на следующий день после отправки статьи осознал грубую ошибку в своих размышлениях. Лучше читать сразу конец статьи… и, да, к copy elision, выходит, статья относиться лишь косвенно.



1. Проблема


Умные указатели для проекта были сделаны ещё летом прошлого года.

Избранный код указателей и пояснения
template<typename T_Type, typename T_Holding, typename T_Access>
class DReference {

. . .
	IDSharedMemoryHolder *_holder;

	void retain(IDSharedMemoryHolder *inHolder) { . . . }
	void release() { . . . }

 . . .

	~DReference() { release(); }

	template<typename T_OtherType,
			typename T_OtherHolding,
			typename T_OtherAccess>
	DReference(
			const DReference<T_OtherType, T_OtherHolding, T_OtherAccess> &
					inLReference) : _holder(NULL), _holding(), _access()
	{
		retain(inLReference._holder);
	}

. . . 

}

У нас есть структуры-стратегии, которые реализуют логику хранения объекта и логику доступа к объекту. Мы передаём их типы в качестве шаблонных аргументов класса умного указателя. IDSharedMemoryHolder — интерфейс доступа к память объекта. Через вызов функции retain() умный указатель начинает владеть объектом (для strong reference-а ++ref_count). По вызову release() указатель освобождает объект (для strong reference-а --ref_count и удаление объекта если ref_count == 0).

Я намеренно опустил здесь вещи, связанные с разыменованием и с retain по вызовам операторов. Описываемая проблема этих моментов не касается.

Работа умных указателей проверялась рядом простых тестов: «создали объект, связанный с указателем — присвоили указателю указатель — глянули, чтобы reatin/release прошли правильно». Тесты (что сейчас кажется очень странным) проходили. Код на умные указатели я перевёл в начале января и… да, тогда всё тоже работало.

Проблемы начались месяц назад, когда обнаружилось, что память, контролируемая умными указателями, удалялась раньше времени.

Поясню на конкретном примере:

DStrongReference<DPlugIn> DPlugInManager::createPlugIn(
		const DPlugInDescriptor &inDescriptor)
{
. . .

	DStrongReference<DPlugIn> thePlugInReference =
			internalCreatePlugIn(inDescriptor);
. . .

	return thePlugInReference;
}

...

DStrongReference<DPlugIn> DPlugInManager::internalCreatePlugIn(
		const DPlugInDescriptor &inDescriptor)
{
	for (IDPlugInStorage *thePlugInStorage : _storages) {
		if (thePlugInStorage->getPlugInStatus(inDescriptor))
			return thePlugInStorage->createPlugIn(inDescriptor);
	}
	return DStrongReference<DPlugIn>();
}

...

class DPlugInStorageImpl : public IDPlugInStorage {
public:
	virtual ~DPlugInStorageImpl() { }

	virtual DStrongReference<DPlugIn> createPlugIn(
			const DPlugInDescriptor &inDescriptor);
};

При вызове метода DPlugInStorageImpl::createPlugIn(...) создавался объект, возвращаемый через DStrongReference, после чего этот умный указатель возвращался через метод DPlugInManager::internalCreatePlugIn(...) в контекст вызова — метод DPlugInManager::createPlugIn(...).

Так вот, когда умный указатель возвращался в метод DPlugInManager::createPlugIn(...), thePlugInReference указывал на удалённый объект. Очевидно, дело было в неправильном количестве retain/release-вызовов. Потратив кучу нервов с дебаггером в Eclipse (к слову — он ужасен), я плюнул, и решил проблему по-простому — использовал лог. Поставил на вызовах методов retain и release вывод, запустил программу… Что я ожидал увидеть? Вот такое что-то (псевдокод):

DPlugInStorageImpl::createPlugIn(...) => RETAIN
DPlugInManager::internalCreatePlugIn(...), return createPlugIn => RETAIN
DPlugInStorageImpl::createPlugIn(...), ~DStrongReference() => RELEASE
DPlugInManager::createPlugIn(...), thePlugInReference = internalCreatePlugIn(...) => RETAIN
DPlugInManager::internalCreatePlugIn(...),~DStrongReference() => RELEASE


Итого: ref_count = 1 для thePlugInReference. Всё должно было быть чётко.

То, что я увидел на самом деле, заставило меня сделать вот так (0_0) и потратить следующие полтора часа пять минут на всякие clean-up, перекомпиляции, перепроверки настроек оптимизации, попытки флашить stdout и проч.

DPlugInStorageImpl::createPlugIn(...) => RETAIN
DPlugInManager::internalCreatePlugIn(...),~DStrongReference() => RELEASE


Отчаявшись решить проблему в боевом коде и уже подозревая что-то крайне неладное, я создал маленький тестовый проект.

2. Тест


Тестовый код:

#include <iostream>
#include <stdio.h>

class TestClass {
private:
	int _state;

public:
	TestClass(int inState) : _state(inState) { std::cout << "State based: " << inState << std::endl; }

	TestClass() : _state(1) { std::cout << "Default" << std::endl; }

	TestClass(const TestClass &inObject0) : _state(2) { std::cout << "Const Copy" << std::endl; }
	TestClass(TestClass &inObject0) : _state(3) { std::cout << "Copy" << std::endl; }

	TestClass(const TestClass &&inObject0) : _state(4) { std::cout << "Const Move" << std::endl; }
	TestClass(TestClass &&inObject0) : _state(5) { std::cout << "Move" << std::endl; }

	~TestClass() { std::cout << "Destroy" << std::endl; }

	void call() { std::cout << "Call " << _state << std::endl; }
};

///////////////////////////////////////////////////////////////////////////////
int main() {
	TestClass theTestObject = TestClass();
	theTestObject.call();
	fflush(stdout);
	return 0;
}

Ожидаемый результат:

Default
Const Copy
Call 1
Destroy


Реальный результат:

Default
Call 1
Destroy


То есть копи-конструктор не вызывался. И только тогда я сделал то, что нужно было сделать сразу. Загуглил и узнал про copy_elision.

3. Страшная правда


В дв��х словах — любой компилятор С++ может без предупреждения и каких-либо флагов игнорировать вызов copy-конструктора и вместо этого, например, напрямую копировать полное состояние объекта. При этом выполнить какую-либо логику в процессе такого копирования без хаков просто так нельзя. Вот тут в разделе Notes прямо сказано: "Элизия копирования — единственный разрешённый вид оптимизации, который может иметь наблюдаемые побочные эффекты", «Copy elision is the only allowed form of optimization that can change the observable side-effects».

Оптимизация — это, конечно, отлично… Но что если мне нужно выполнять какую-нибудь логику в конструкторе копирования. Например, для умных указателей? И что мне до сих пор непонятно, почему нельзя было разрешить выполнять подобную оптимизацию при -o1 в случае, если в теле copy-конструктора нет никакой логики?.. До сих пор сие мне не ясно.

4. Решение


Я нашёл два способа заставить компилятор выполнять логику в момент конструирования объектов класса:

1) Через флаги компиляции. Плохой способ. Компиляторозависимый. Например, для g++ нужно ставить флаг -fno-elide-constructors, и это либо будет влиять на весь проект (что ужасно), либо придётся использовать в соответствующих местах push/pop настройки флагов компилятора, что загромождает код и делает менее читабельным (особенно с учётом того, что такое придётся делать под каждый компилятор).

2) Через ключевое слово explicit. Это тоже плохой способ, но, на мой взгляд, это лучше, че�� использовать флаги компиляции.
Спецификатор explicit нужен, чтобы запретить неявное создание экземпляров класса через синтаксис приведения типов. То есть, для того, чтобы вместо MyInt theMyInt = 1 нужно было обязательно писать MyInt theMyInt = MyInt(1).
В случае, если выставить это слово перед copy-конструктором, мы получим достаточно забавный запрет неявного приведения типа — запрет приведения к своему типу.

Таким образом, например, следующий

код
#include <iostream>
#include <stdio.h>

class TestClass {
private:
	int _state;

public:
	TestClass(int inState) : _state(inState) { std::cout << "State based: " << inState << std::endl; }

	TestClass() : _state(1) { std::cout << "Default" << std::endl; }
	explicit TestClass(const TestClass &inObject0) : _state(2) { std::cout << "Const Copy" << std::endl; }
}

	~TestClass() { std::cout << "Destroy" << std::endl; }

	void call() { std::cout << "Call " << _state << std::endl; }
};

///////////////////////////////////////////////////////////////////////////////
int main() {
	TestClass theTestObject = TestClass();
	theTestObject.call();
	fflush(stdout);
	return 0;
}


у меня (g++ 4.6.1) вызвал ошибку:

error: no matching function for call to 'TestClass::TestClass(TestClass)'

Что ещё забавнее, из-за особенностей синтаксиса С++ вот так: TestClass theTestObject(TestClass()) записать тоже не выйдет, ведь это будет считаться объявлением указателя на функцию и вызовет ошибку:

error: request for member 'call' in 'theTestObject', which is of non-class type 'TestClass(TestClass (*)())'

Таким образом, мы вместо того, чтобы заставить компилятор выполнять конструктор копирования, запретили вызывать этот конструктор.

К счастью для меня, такое решение подошло. Дело в том, что запретив конструктор копирования я вынудил компилятор использовать спецификацию шаблонного конструктора с теми же шаблонными аргументами, что и у текущего класса. То есть это не было «приведение объекта к своему типу», а это было «приведение к типу, у которого те же шаблонные аргументы», что порождает ещё один метод, но заменяет конструктор копирования.

Вот что вышло
template<typename T_Type, typename T_Holding, typename T_Access>
class DReference {

. . .
	IDSharedMemoryHolder *_holder;

	void retain(IDSharedMemoryHolder *inHolder) { . . . }
	void release() { . . . }

 . . .

	~DReference() { release(); }

	//NB: Workaround for Copy elision
	explicit DReference(
			const OwnType &inLReference)
					: _holder(NULL), _holding(), _access()
	{
		// Call for some magic cases
		retain(inLReference._holder);
	}

	template<typename T_OtherType,
			typename T_OtherHolding,
			typename T_OtherAccess>
	DReference(
			const DReference<T_OtherType, T_OtherHolding, T_OtherAccess> &
					inLReference) : _holder(NULL), _holding(), _access()
	{
		retain(inLReference._holder);
	}

. . . 

}



Для тестового примера аналог этого костыля выглядел бы вот так:

Тестовый код и короткое пояснение
#include <iostream>
#include <stdio.h>

class TestClass {
private:
	int _state;

public:
	TestClass(int inState) : _state(inState) { std::cout << "State based: " << inState << std::endl; }

	TestClass() : _state(1) { std::cout << "Default" << std::endl; }
	explicit TestClass(const TestClass &inObject0) : _state(2) { std::cout << "Const Copy" << std::endl; }

	template<typename T>
	TestClass(const T &inObject0) : _state(13) { std::cout << "Template Copy" << std::endl; }

	~TestClass() { std::cout << "Destroy" << std::endl; }

	void call() { std::cout << "Call " << _state << std::endl; }
};

///////////////////////////////////////////////////////////////////////////////
int main() {
	TestClass theTestObject = TestClass();
	theTestObject.call();
	fflush(stdout);
	return 0;
}


Та же фишка. Спецификация шаблона, заменяющая конструктор копирования… Тут видно, что это плохое решение, ведь мы не к месту использовали шаблоны. Если кто знает как лучше — отпишитесь.


Вместо заключения


Когда я рассказал о copy elision нескольким знакомым, которые года три в С++ и около-С++ разработке, они тоже сделали вот так (0_0) удивились не меньше моего. Между тем, данная оптимизация может породить поведение, странное с точки зрения программиста, и вызвать ошибки при написании С++ приложений.

Надеюсь, данная статья кому-нибудь пригодится и сэкономит чьё-нибудь время.

П.С.: Пишите по поводу замеченных оплошностей — буду править.

Редактировано:



Комментирующие правы, я вообще неправильно понял проблему. Спасибо всем, особенно Monnoroch за выявление логических ошибок в статье.

Написав следующий тестовый код, я получил корректный output:

Template-класс с copy-конструктором и output
///////////////////////////////////////////////////////////////////////////////
#include <iostream>

///////////////////////////////////////////////////////////////////////////////
template<typename T_Type>
class TestTemplateClass {
private:
	typedef TestTemplateClass<T_Type> OwnType;

    T_Type _state;

public:
    TestTemplateClass() : _state() {
        std::cout << "Default constructor" << std::endl;
    }

    TestTemplateClass(int inState) : _state(inState) {
        std::cout << "State constructor" << std::endl;
    }

    TestTemplateClass(const OwnType &inValue) {
        std::cout << "Copy constructor" << std::endl;
    }

    template<typename T_OtherType>
    TestTemplateClass(const TestTemplateClass<T_OtherType> &inValue) {
        std::cout << "Template-copy constructor" << std::endl;
    }

    template<typename T_OtherType>
    void operator = (const TestTemplateClass<T_OtherType> &inValue) {
        std::cout << "Operator" << std::endl;
    }

    ~TestTemplateClass() {
        std::cout << "Destructor" << std::endl;
    }
};

///////////////////////////////////////////////////////////////////////////////
TestTemplateClass<int> createFunction() {
    return TestTemplateClass<int>();
}

///////////////////////////////////////////////////////////////////////////////
int main() {
    TestTemplateClass<int> theReference = createFunction();
    std::cout << "Finished" << std::endl;
    return 0;
}

///////////////////////////////////////////////////////////////////////////////


Output:
Default constructor
Copy constructor
Destructor
Copy constructor
Destructor
Finished
Destructor



То есть действительно, проблема была не в copy elision и не нужно никаких хаков.

Действительная же ошибка оказалась банальной. Сейчас стыдно за то, что я взялся писать статью, не проверив всё как следует.
Дело в том, что умные указатели принимают три шаблонных аргумента:

template<typename T_Type, typename T_Holding, typename T_Access>
class DReference {
     . . . 


  1. T_Type — тип объекта, который управляется системой умных указателей.
  2. T_Holding — стратегия владения памятью.
  3. T_Access — стратегия доступа к памяти.


Такой вариант реализации умных указателей позволяет сделать гибкой настройку их поведения, но при этом делает громоздким их использование (особенно с учётом того, что стратегии также шаблонные классы).

Пример объявления strong-указателя:

DReference<MyType, DReferenceStrongHolding<MyType>, DReferenceCachingAccess< MyType > > theReference;


Чтобы избежать загромождения кода, я хотел использовать фичу стандар��а С++11 — template-alias. Но, как оказалось, g++ 4.6.1 их не поддерживает. Конечно, когда пишешь свой домашний pet-project возиться с настройкой среды лень, поэтому я решил сделать очередной workaround и избавиться от аргумента с помощью наследования:

template<typename T_Type>
class DStrongReference : public DReference< T_Type, DReferenceStrongHolding<MyType>, DReferenceCachingAccess< MyType > > {
     . . .


При этом нужно было определить кучу конструкторов для DStrongReference, вызывающих из себя соответствующие конструкторы базового класса DReference — ведь конструкторы не наследуются. И, конечно, я пропустил конструктор копирования… В общем, единственный совет, который могу дать после всех этих приключений — нужно быть очень внимательным при использовании шаблонов, чтобы не попасть в такую глупую ситуацию, в которую попал я.

P.S.: Вот тест, который использует наследование для замены template-alias (спасибо ToSHiC за дельный совет передавать this в output):

Тестовая имитация template-alias
///////////////////////////////////////////////////////////////////////////////
#include <iostream>

///////////////////////////////////////////////////////////////////////////////
template<typename T_Type, typename T_Strategy>
class TestTemplateClass {
private:
	typedef TestTemplateClass<T_Type, T_Strategy> OwnType;

	T_Type _state;
	T_Strategy _strategy;

public:
	TestTemplateClass() : _state(), _strategy() {
		std::cout << "Default constructor: " << this << std::endl;
	}

	TestTemplateClass(int inState) : _state(inState), _strategy() {
		std::cout << "State constructor: " << this << std::endl;
	}

	TestTemplateClass(const OwnType &inValue)
		: _state(), _strategy()
	{
		std::cout << "Copy constructor: " << this << " from " <<
				&inValue << std::endl;
	}

	template<typename T_OtherType, typename T_OtherStrategy>
	TestTemplateClass(
			const TestTemplateClass<T_OtherType, T_OtherStrategy> &inValue)
		: _state(), _strategy()
	{
		std::cout << "Template-copy constructor: " << this << std::endl;
	}

	void operator = (const OwnType &inValue) {
		std::cout << "Assigning: " << this << " from " << inValue << std::endl;
	}

	template<typename T_OtherType, typename T_OtherStrategy>
	void operator = (
			const TestTemplateClass<T_OtherType, T_OtherStrategy> &inValue)
	{
		std::cout << "Assigning: " << this << " from "
				<< &inValue << std::endl;
	}

	~TestTemplateClass() {
		std::cout << "Destructor: " << this << std::endl;
	}
};

///////////////////////////////////////////////////////////////////////////////
template<typename T_Type>
class TestTemplateClassIntStrategy : public TestTemplateClass<T_Type, int> {
private:
	//- Types
	typedef TestTemplateClassIntStrategy<T_Type> OwnType;
	typedef TestTemplateClass<T_Type, int> ParentType;

public:
	TestTemplateClassIntStrategy() : ParentType() { }
	TestTemplateClassIntStrategy(int inState) : ParentType(inState) { }

	TestTemplateClassIntStrategy(const OwnType &inValue)
		: ParentType(inValue) { }

	template<typename T_OtherType, typename T_OtherStrategy>
	TestTemplateClassIntStrategy(
			const TestTemplateClass<T_OtherType, T_OtherStrategy> &inValue)
		: ParentType(inValue) { }

	//- Operators
	void operator = (const OwnType &inValue) {
		ParentType::operator =(inValue);
	}

	template<typename T_OtherType, typename T_OtherStrategy>
	void operator = (
			const TestTemplateClass<T_OtherType, T_OtherStrategy> &inValue)
	{
		ParentType::operator =(inValue);
	}
};

///////////////////////////////////////////////////////////////////////////////
TestTemplateClassIntStrategy<int> createFunction() {
	return TestTemplateClassIntStrategy<int>();
}

int main() {
	TestTemplateClassIntStrategy<int> theReference = createFunction();
	std::cout << "Finished" << std::endl;
	return 0;
}


Output:
Default constructor: 0x28fed8
Copy constructor: 0x28ff08 from 0x28fed8
Destructor: 0x28fed8
Copy constructor: 0x28ff00 from 0x28ff08
Destructor: 0x28ff08
Finished
Destructor: 0x28ff00



Вызов оператора присвоения
. . .

int main() {
	TestTemplateClassIntStrategy<int> theReference;
	theReference = createFunction();
	std::cout << "Finished" << std::endl;
	return 0;
}


Default constructor: 0x28ff00
Default constructor: 0x28fed8
Copy constructor: 0x28ff08 from 0x28fed8
Destructor: 0x28fed8
Assigning: 0x28ff00 from 0x28ff08
Destructor: 0x28ff08
Finished
Destructor: 0x28ff00



Важный минус этого способа: если таким образом определить strong-pointer и weak-pointer, они будут совершенно разных типов (не связанных даже с одним template-классом) и не выйдет их присваивать один другому напрямую в момент инициализации.

<Редактировано №2>

Снова поторопился что-то утверждать. Ночью понял. Выйдет ведь… У этих классов один общий шаблонный предок.
То есть если в наследнике (который имитирует template-alias) будет описан шаблонный конструктор от произвольного DReference, в следующем коде всё будет нормально:

Код
DStrongReference<Type> theStrongReference;

// В следующей строке будет вызван шаблонный конструктор от базового шаблонного класса. Вот этот:
//
// template<typename Type, typename Owning, typename Holding>
// DWeakReference::DWeakReference(const DReference<Type, Owning, Holding> &ref) : Parent(ref) { }
//
// Этот конструктор будет вызван за счёт того, что DStrongReference наследует DReference.
//
DWeakReference<Type> theWeakReference = theStrongReference;



Тестовый код для двух классов, организованных таким образом:

template-alias через наследование: два псевдо-alias
//============================================================================
// Name        : demiurg_application_example.cpp
// Author      : 
// Version     :
// Copyright   : Your copyright notice
// Description : Hello World in C++, Ansi-style
//============================================================================

///////////////////////////////////////////////////////////////////////////////
#include <iostream>

///////////////////////////////////////////////////////////////////////////////
template<typename T_Type, typename T_Strategy>
class TestTemplateClass {
private:
	typedef TestTemplateClass<T_Type, T_Strategy> OwnType;

	T_Type _state;
	T_Strategy _strategy;

public:
	TestTemplateClass() : _state(), _strategy() {
		std::cout << "Default constructor: " << this << std::endl;
	}

	TestTemplateClass(int inState) : _state(inState), _strategy() {
		std::cout << "State constructor: " << this << std::endl;
	}

	TestTemplateClass(const OwnType &inValue)
		: _state(), _strategy()
	{
		std::cout << "Copy constructor: " << this << " from " <<
				&inValue << std::endl;
	}

	template<typename T_OtherType, typename T_OtherStrategy>
	TestTemplateClass(
			const TestTemplateClass<T_OtherType, T_OtherStrategy> &inValue)
		: _state(), _strategy()
	{
		std::cout << "Template-copy constructor: " << this << std::endl;
	}

	void operator = (const OwnType &inValue) {
		std::cout << "Assigning: " << this << " from " << &inValue << std::endl;
	}

	template<typename T_OtherType, typename T_OtherStrategy>
	void operator = (
			const TestTemplateClass<T_OtherType, T_OtherStrategy> &inValue)
	{
		std::cout << "Assigning: " << this << " from "
				<< &inValue << std::endl;
	}

	~TestTemplateClass() {
		std::cout << "Destructor: " << this << std::endl;
	}
};

///////////////////////////////////////////////////////////////////////////////
//- Integer strategy
template<typename T_Type>
class TestTemplateClassIntStrategy : public TestTemplateClass<T_Type, int> {
private:
	//- Types
	typedef TestTemplateClassIntStrategy<T_Type> OwnType;
	typedef TestTemplateClass<T_Type, int> ParentType;

public:
	TestTemplateClassIntStrategy() : ParentType() { }
	TestTemplateClassIntStrategy(int inState) : ParentType(inState) { }

	TestTemplateClassIntStrategy(const OwnType &inValue)
		: ParentType(inValue) { }

	template<typename T_OtherType, typename T_OtherStrategy>
	TestTemplateClassIntStrategy(
			const TestTemplateClass<T_OtherType, T_OtherStrategy> &inValue)
		: ParentType(inValue) { }

	//- Operators
	void operator = (const OwnType &inValue) {
		ParentType::operator =(inValue);
	}

	template<typename T_OtherType, typename T_OtherStrategy>
	void operator = (
			const TestTemplateClass<T_OtherType, T_OtherStrategy> &inValue)
	{
		ParentType::operator =(inValue);
	}
};

//- Boolean strategy
template<typename T_Type>
class TestTemplateClassBoolStrategy : public TestTemplateClass<T_Type, bool> {
private:
	//- Types
	typedef TestTemplateClassBoolStrategy<T_Type> OwnType;
	typedef TestTemplateClass<T_Type, bool> ParentType;

public:
	TestTemplateClassBoolStrategy() : ParentType() { }
	TestTemplateClassBoolStrategy(int inState) : ParentType(inState) { }

	TestTemplateClassBoolStrategy(const OwnType &inValue)
		: ParentType(inValue) { }

	template<typename T_OtherType, typename T_OtherStrategy>
	TestTemplateClassBoolStrategy(
			const TestTemplateClass<T_OtherType, T_OtherStrategy> &inValue)
		: ParentType(inValue) { }

	//- Operators
	void operator = (const OwnType &inValue) {
		ParentType::operator =(inValue);
	}

	template<typename T_OtherType, typename T_OtherStrategy>
	void operator = (
			const TestTemplateClass<T_OtherType, T_OtherStrategy> &inValue)
	{
		ParentType::operator =(inValue);
	}
};

///////////////////////////////////////////////////////////////////////////////
TestTemplateClassBoolStrategy<int> createFunction() {
	return TestTemplateClassBoolStrategy<int>();
}

int main() {
	TestTemplateClassIntStrategy<int> theReference;
	theReference = createFunction();
	std::cout << "Finished" << std::endl;
	return 0;
}


Output:
Default constructor: 0x28fed8
Copy constructor: 0x28ff08 from 0x28fed8
Destructor: 0x28fed8
Copy constructor: 0x28ff00 from 0x28ff08
Destructor: 0x28ff08
Finished
Destructor: 0x28ff00



В общем, всё работает

</Редактировано №2>

Спасибо 1eqinfinity, Torvald3d за указание орфографических ошибок.