If I compile and run the following:
using namespace System;
ref class C1
{
public:
C1()
{
Console::WriteLine(L"Creating C1");
}
protected:
~C1()
{
Console::WriteLine(L"Destroying C1");
}
};
int main(array<System::String ^> ^args)
{
C1^ c1 = gcnew C1();
delete c1;
return 0;
}
...the code compiles without an error and runs giving me this:
Creating C1
Destroying C1
Press any key to continue . . .
If I do the same in C++ I get an error along the following lines:
1>ProtectedDestructor.cpp(45): error C2248: 'C1::~C1' : cannot access protected member declared in class 'C1'
1> ProtectedDestructor.cpp(35) : compiler has generated 'C1::~C1' here
1> ProtectedDestructor.cpp(23) : see declaration of 'C1'
...so why is it valid in CLI?
This is a leaky abstraction problem. C++/CLI has several of them, we already went through the const keyword problem. Much the same here, the runtime does not have any notion of a destructor, only the finalizer is real. So it has to be faked. It was pretty important to create that illusion, the RAII pattern in native C++ is holy.
It is faked by bolting the notion of a destructor on top of the IDisposable interface. The one that makes deterministic destruction work in .NET. Very common, the using keyword in the C# language invokes it for example. No such keyword in C++/CLI, you use the delete
operator. Just like you would in native C++. And the compiler helps, automatically emitting the destructor calls when you use stack semantics. Just like a native C++ compiler does. Rescuing RAII.
Decent abstraction, but yes, it leaks. Problem is that an interface method is always public. It is technically possible to make it private with explicit interface implementation although it is but a stopgap:
public ref class Foo : IDisposable {
protected:
//~Foo() {}
virtual void Dispose() = IDisposable::Dispose {}
};
Produces a very impressive error list when you try this, the compiler fights back as hard as it can :). C2605 is the only relevant one: "'Dispose': this method is reserved within a managed class". It can't maintain the illusion when you do this.
Long story short, the IDisposable::Dispose() method implementation is always public, regardless of the accessibility of the destructor. The delete
operator invokes it. No workaround for this.
In addition to Hans's detailed answer that delete
on a C++/CLI object is actually activation of the IDisposable
interface, and interface inheritance is always public1, it may be fruitful to ask
How does the protected destructor get called, then?
The compiler-generated Dispose
methods call the user-defined destructor. Because this Dispose
method is a member of the class, it has access to protected
and private
class members, such as the destructor.
(In native C++, the compiler isn't subject to accessibility rules, since it is the one enforcing them. In .NET, the IL verifier enforces them.)
1 Actually, his explanation centers on the fact that the compiler doesn't allow explicit implementation of IDisposable::Dispose()
, in which case it could be a private member. But that's completely irrelevant. virtual
members can be reached through the declaring type. And delete
doesn't call object->Dispose()
, it calls safe_cast<IDisposable^>(object)->Dispose()
.