views:

131

answers:

2

I'm used to the C++ RAII facilities, and I want to use RAII the right way with managed code in C++/CLI. Herb Sutter and Microsoft both tell me this is the best practice.

I have something like this:

ref struct Managed
{
    // No default constructor
    Managed( /*...*/ ) { /*...*/ }
    ~Managed() { /* Important non-managed resource release here */ }
    // ...
};

ref struct UsesManaged
{
    Managed^         m_;
    array<Managed^>^ a_;

    UsesManaged( Managed^ m, array<Managed^>^ a ) : m_(m), a_(a) {}
    // ...
};

ref struct Creator
{
    Managed^         m_;
    array<Managed^>^ a_;
    UsesManaged^     u_;

    Creator()
    {
        // Must allocate dynamically here, not in initializer list
        // because in my real code, I use "this" here for a callback.
        m_      = gcnew Managed( /*...*/ );
        a_      = gcnew array<Managed^>( 2 );
        a_[ 0 ] = gcnew Managed( /*...*/ );
        a_[ 1 ] = gcnew Managed( /*...*/ );
        u_      = gcnew UsesManaged( m_, a_ );
    }
};

I want (1) automatic resource destruction so I don't have to delete every gcnew'ed object manually, particularly in the face of exceptions; (2) the ability to share objects safely and clearly (passing around std::auto_ptr and the like doesn't qualify); and (3) the ability to have my class consumed by VB or C# and have the cleanup automatically run when the object goes out of scope (e.g., due to an exception).

In standard C++ I'd use std::shared_ptr and std::vector or similar facilities to automate RAII. Here, I could use STL/CLI's vector, but there is no shared_ptr equivalent. The only relevant C++/CLI smart pointer I see is the sparsely documented msclr::auto_handle, which is akin to std::auto_ptr, including transfer-of-ownership semantics, which are not compatible with vectors, though they'd work alright in an array.

What's the proper C++/CLI way to achieve my three goals? (Note also, my main C++/CLI class, Creator in the above, will be consumed by VB/C#.)

[Updates: Added links to Herb Sutter and MS at the top and added goal 3 (consumption by VB/C#).]

+1  A: 

You can have RAII with managed code: if you have this:

ref class A {
  ~A() { // implements/overrides the IDisposable::Dispose method
        // free managed and unmanaged resources here
   }
};


Then you can do this:

void foo()
{
  A a(cons_args); // stack-like usage
  // use a ...
}

and this will effectively be treated as:

void foo()
{
  try
  {
     A^ a_ = gcnew A(cons_args);
  }
  finally
  {
     a_->~A();
  }
}
tragomaskhalos
+1, yup, this is the way to do it. I dislike the syntax intensely, it's a major bug generator and gives the newbies very little chance to guess when they have to use the caret. But so be it.
Hans Passant
+1 -- but I still think anyone doing this needs to read http://msdn.microsoft.com/en-us/library/ms177197.aspx
Billy ONeal
Right, but as I hinted at in the comment in Creator's constructor, I have to use dynamic allocation rather than "GC stack" allocation. What I really need is a C++/CLI equivalent to std::shared_ptr.
mlimber
@mclimber: Ah -- you can't do that. There is no way to force the garbage collector to destroy an object. See http://blogs.msdn.com/b/oldnewthing/archive/2010/08/09/10047586.aspx The right way to do that is to require an explicit close call, and to put a call to that in a finalizer. If for some reason the client code doesn't clean up, the GC will (eventually) clean it up for you (at program termination if nothing else), but you cannot write code that depends on that.
Billy ONeal
@Billy: You can't have deterministic deallocation, but you can have deterministic disposal. With stack-semantics, the disposer (which has C++ destructor naming, ~Class) is automatically called when the parent object is disposed (or for local objects, when the scope ends). It definitely would be possible to implement an `auto_handle` variant (`counted_handle`?) which acts like `std::shared_ptr`, but I do not think that Microsoft provided one.
Ben Voigt
Even if I roll my own reference-counting pointer for managed types?
mlimber
Thanks, Ben. You beat me to it! Billy, do you agree with Ben?
mlimber
@Hans: stack semantics are much easier to get right than C# `using`. In C++/CLI, the only question is object lifetime. In C#, you have to not only think about object lifetime but also whether the object needs to be explicitly disposed (and if resources are added later, a huge amount of code has to change). In C++/CLI, you just use the stack syntax for all temporary helper objects and the compiler works out whether there's a Dispose method to call. Add polymorphism (`IDispose` may or may not be implemented by the derived class) and C++/CLI is the only workable approach.
Ben Voigt
@mlimber: I don't agree nor do I disagree with Ben. I'm not sure how such refcounting would be implemented in C++/CLI, because normal C++ refcounting is based on the number of objects alive at any given point. But with managed handles, that idea no longer makes sense (because someone can copy a reference without notifying the object in question). I would ask Ben :)
Billy ONeal
@Billy: It's really the same situation as in standard C++, where you can copy pointers all day long and no one can keep a count. But if you use smart pointer objects, they can adjust the counter when copied, assigned, or go out of scope. Same here.
Ben Voigt
Also, there's a bug in tragomaskhalos's last bit of code above. Namely, the gcnew should be OUTSIDE the try block. You shouldn't call the destructor if the memory allocation or the constructor failed (i.e., threw an exception) since no object was created and so can't be destructed. Moreover it's out of scope in the finally block, so not only shouldn't you try it but you actually can't.
mlimber
+1  A: 

Not tested, but this should get you started:

template<typename T>
value class counted_handle
{
    ref struct Count { int refCount; Count() : refCount(1) {} };
    T^ m_sharedHandle;
    Count^ m_sharedCount;

    void release() { if (m_sharedCount && 0 == --sharedCount->refCount) delete m_sharedHandle; m_sharedCount = nullptr; m_sharedHandle = nullptr; }
    void addref( if (m_sharedCount) ++m_sharedCount->refCount; }
public:
    counted_handle() : m_sharedHandle(nullptr), m_sharedCount(nullptr) {}
    counted_handle(T^ handle) : m_sharedHandle(handle), m_sharedCount(gcnew Count()) {}
    counted_handle(counted_handle<T>% src) : m_sharedHandle(src.m_sharedHandle), m_sharedCount(src.sharedCount) { addref(); }
    void ~counted_handle() { release(); }
    counted_handle<T>% operator=(counted_handle<T>% src) { release(); m_sharedHandle = src.m_sharedHandle; m_sharedCount = src.m_sharedCount; addref(); }
    counted_handle<T>% operator=(T^ handle) { release(); m_sharedHandle = handle; m_sharedCount = gcnew Count(); }
}
Ben Voigt