Emulating the C# using keyword in C++


C# has a very convenient using statement which ensures that an object is Dispose()d when control exits the block. C++ has a generalization of this concept with RAII types, but things get tricky when you have tasks, lambda capture, and the need to explicitly Close() the hat pointer.

Here we go.

Here's one attempt, for C++/CX hat pointers:

template<typename T>
class unique_close
{
public:
  unique_close(T^ t) : m_t(t) { }
  ~unique_close() { delete m_t; }

  // Disallow copying
  unique_close(const unique_close& other) = delete;
  unique_close& operator=(const unique_close& other) = delete;

  // Moving transfers the obligation to Close
  unique_close(unique_close&& other)
  {
    *this = std::move(other);
  }
  unique_close& operator=(unique_close&& other)
  {
    using std::swap; // enable ADL on the swap
    swap(m_t, other.m_t);
    return *this;
  }

private:
  T^ m_t;
};

template<typename T>
auto make_unique_close(T^ t)
{
  return unique_close<T>(t);
}

With explicit task chaining, you need to remember to use std::move to move the unique_close object into the final task in the chain if you didn't construct it directly in the capture. If you forget to do this, then the destruction of the unique_close objects in the main function will close the objects prematurely because they are still in control.

void Scenario1_Render::ViewPage()
{
    ...

    if (outfile)
    {
        auto page = pdfDocument->GetPage(pageIndex);

        return create_task(outfile->OpenTransactedWriteAsync())
            .then([this, page, usingPage{make_unique_close(page)}]
                  (StorageStreamTransaction^ transaction) mutable
        {
            auto options = ref new PdfPageRenderOptions();
            options->DestinationHeight = (unsigned)(page->Size.Height * 2);
            options->DestinationWidth = (unsigned)(page->Size.Width * 2);
            create_task(page->RenderToStreamAsync(transaction->Stream, options))
                .then([this, usingPage{std::move(usingPage)},
                       usingTransaction{make_unique_close(transaction)}]() mutable
            {
                // destruction of usingPage and usingTransaction
                // will close the page and transaction.
            });
        });
    }
    ...
}

(Let us ignore the fact that this doesn't work because task::then requires a copyable lambda, and ours is merely movable.)

One way to address the added cognitive burden of having to remember to keep moving the obligation to close is to share that obligation, so that only when the last shared reference is destructed does the object get closed.

template<typename T>
class ensure_close
{
public:
  ensure_close(T^ t) : m_t(t) { }
  ~ensure_close() { delete m_t; }

  // Disallow copying and moving
  ensure_close(const ensure_close& other) = delete;
  ensure_close& operator=(const ensure_close& other) = delete;
  ensure_close(const ensure_close&& other) = delete;
  ensure_close& operator=(const ensure_close&& other) = delete;

private:
  T^ m_t;
};

template<typename T>
using shared_close = std::shared_ptr<ensure_close<T>>;

template<typename T>
auto make_shared_close(T^ t)
{
  return std::make_shared<ensure_close<T>>(t);
}

Now you can copy the shared_close around, and only when the last copy is destructed does the wrapped hat pointer get closed.¹

void Scenario1_Render::ViewPage()
{
    ...

    if (outfile)
    {
        auto page = pdfDocument->GetPage(pageIndex);
        auto usingPage = make_shared_close(page);

        return create_task(outfile->OpenTransactedWriteAsync())
            .then([this, page, usingPage]
                  (StorageStreamTransaction^ transaction)
        {
            auto usingTransaction = make_shared_close(transaction);
            auto options = ref new PdfPageRenderOptions();
            options->DestinationHeight = (unsigned)(page->Size.Height * 2);
            options->DestinationWidth = (unsigned)(page->Size.Width * 2);
            return create_task(page->RenderToStreamAsync(transaction->Stream, options))
                .then([this, usingPage, usingTransaction]()
            {
                // destruction of the last shared usingPage
                // and usingTransaction will close the page and transaction.
            });
        });
    }
    ...
}

Still, you have to remember to keep passing the usingPage and usingTransaction around. If you forget, then the object gets closed prematurely. (And if the shared_close is created by one lambda and consumed by a sibling lambda, well you now have a shared_ptr to a shared_close, which is getting ridiculous.)

But wait, you can stop the madness.

Let's go back to unique_close: This class becomes much more convenient if you use the co_await keyword, because the compiler will do the heavy lifting of cleaning up when the last task has completed.²

task<void> Scenario1_Render::ViewPageAsync()
{
    ...

    if (outfile)
    {
        auto page = pdfDocument->GetPage(pageIndex);
        auto usingPage = make_unique_close(page);

        auto transaction =
            co_await outfile->OpenTransactedWriteAsync();
        auto usingTransaction = make_unique_close(transaction);
        auto options = ref new PdfPageRenderOptions();
        options->DestinationHeight = (unsigned)(page->Size.Height * 2);
        options->DestinationWidth = (unsigned)(page->Size.Width * 2);
        co_await page->RenderToStreamAsync(transaction->Stream, options);
        // destruction of usingPage and usingTransaction
        // will close the page and transaction.
    }
    ...
}

We have offloaded all the thinking to the compiler. The compiler will do the work of making sure that the unique_close objects are destructed when control leaves the block. The unique_close objects will remain alive during the co_await statements, which is what we want.

We could make our unique_close a little fancier by making it a little more unique_ptry.

template<typename T>
class unique_close
{
public:
  unique_close(T^ t) : m_t(t) { }
  ~unique_close() { delete m_t; }

  T^ get() { return m_t; }
  T^ operator*() { return m_t; }
  T^ operator->() { return m_t; }
  ...
};

This leaves us with

task<void> Scenario1_Render::ViewPageAsync()
{
    ...

    if (outfile)
    {
        auto page = make_unique_close(
             pdfDocument->GetPage(pageIndex));
        auto transaction = make_unique_close(
            co_await outfile->OpenTransactedWriteAsync());
        auto options = ref new PdfPageRenderOptions();
        options->DestinationHeight = (unsigned)(page->Size.Height * 2);
        options->DestinationWidth = (unsigned)(page->Size.Width * 2);
        co_await page->RenderToStreamAsync(transaction->Stream, options);
        // destruction of page and transaction
        // will close the page and transaction.
    }
    ...
}

That doesn't seem so bad. Pretty close to C# but still in the spirit of C++.³

¹ You (and by "you" I mean "me") would be sorely tempted to write this with a custom deleter instead.

namespace Details
{
  template<typename T>
  struct close_deleter
  {
    void operator()(T^ t) { delete t; }
  };
}

template<typename T>
auto make_shared_close(T^ t)
{
    return std::shared_ptr<T>(t, Details::close_deleter<T>());
}

Except that this doesn't work because std::shared_ptr manages raw pointers, not hat pointers.

² If you are a total crazy person, you might consider adding a boolean conversion operator to the unique_close:

...
   operator bool() const { return true; }
...

This appears to serve no purpose, but it lets you write this:

// Oh my goodness what kind of craziness is about to happen?
#define scope_using__(t, c) \
    if (auto _scope_using_##c##_ = make_unique_close(t))
#define scope_using_(t, c) scope_using__(t, c) 
#define scope_using(t) scope_using(t, __COUNTER__)

task<void> Scenario1_Render::ViewPageAsync()
{
    ...

    if (outfile)
    {
        auto page = pdfDocument->GetPage(pageIndex);
        scope_using (page)
        {
            auto transaction =
                co_await outfile->OpenTransactedWriteAsync();
            scope_using (transaction)
            {
                auto options = ref new PdfPageRenderOptions();
                options->DestinationHeight = (unsigned)(page->Size.Height * 2);
                        options->DestinationWidth = (unsigned)(page->Size.Width * 2);
                co_await page->RenderToStreamAsync(transaction->Stream, options);
                // exiting the scope_using will close
                // the corresponding objcts.
            }
        }
    }
    ...
}

This is a moral outrage. Let us never speak of this again.

³ If you wanted to be cute, you could rename make_unique_close to Using.

auto page = Using(pdfDocument->GetPage(pageIndex));
auto transaction = Using(co_await outfile->OpenTransactedWriteAsync());
Comments (14)
  1. Rob says:

    And here is why I've stopped using c++, preferring c#!

  2. John Schroedl says:

    Some pretty cool techniques here!

  3. CarlD says:

    Nice! A great example and motivation for await and co_await - they make this kind of async continuation based work so much easier to read and write correctly.

  4. Rich says:

    that deserves a round of applause !

  5. Daniel says:

    I don't like the idea of implementing the move constructor using the move assignment operator / std::swap().

    The move constructor is required to leave "other" in a valid (but otherwise unspecified) state. This is so that the destructor can run correctly on the moved-from value. By swapping with an uninitialized *this, you leave behind uninitialized memory in other, and the destructor call has undefined behavior. Now, I'm not sure what the rules with ^ pointers are -- if these are implicitly initialized to nullptr, the code here happens to be fine. But applying the same move constructor pattern to other classes is likely to result in subtle bugs.

    A much less error-prone pattern for move constructors is to move-construct the members and then reset the state in other so that the destructor becomes a no-op:
    unique_close(unique_close&& other)
    : m_t(std::move(other.m_t))
    {
    other.m_t = nullptr;
    }

    1. Hat pointers default-construct as nullptr, so we're okay.

  6. In a way, this post reminds me of my childhood: I used to read computer magazines that had Pascal or C source codes, then try to reproduce them in QBasic! Of course, those programming languages have a very rich and flexible type system and QBasic didn't even have one-byte data types. Oh, the troubles!

    Then I purchased a Borland Pascal license and left the QBasic world behind, just like a baby who leaves the restricted world of womb behind.

  7. ADev says:

    Can't wait for CoreRT to come and finaly get rid of that ancient and ugly C++

  8. kantos says:

    I'm pretty sure you could use the default move assignment and constructor here for unique_close, just declaring them automatically deletes the copy members making the type move only. In essence unique_close(unique_close&& other) = default; should do the trick.

    1. Ben Voigt (Visual Studio and Development Technologies MVP with C++ focus) says:

      No: the default implementation of "move" for primitive types such as handles is to copy the pointer. This would result in double (and premature) free, when the moved-from object runs its destructor.

  9. Evan says:

    I came up with this nice use of the coroutines TS recently (hopefully the formatting comes through):

    template
    auto enumerate(T && sequence)
    {
    int n = 0;
    for (auto && item : sequence) {
    co_yield std::make_pair(n, item);
    ++n;
    }
    }

    I think I should be forwarding to make_pair, and I'm not sure the && on item is right, but I'm not sure how to do this all correctly at this point. This is doable but far far more obnoxious without co_yield.

  10. pm100 says:

    the fact that this idiom keeps getting implemented over and over again just shows how useful 'finally' would be in c++, but they stubbornly refuse to do it. "For the 50th time today - there's no call for it".

  11. SimonRev says:

    I have to confess that I have a LOCK #define for a CRITICAL_SECTION wrapper class that does exactly what your footnote 2 does (I wrote it years ago but still occasionally pull it out).

    It is moral outrage, but still makes the critical section blocks so much easier to follow when reading the code. And I remember clearly when I was puzzling that out and the feeling I had when I realized that I had to add a useless bool conversion operator to make it work.

  12. Ben Voigt (Visual Studio and Development Technologies MVP with C++ focus) says:

    BUG: The unique_close move constructor swaps an uninitialized member with the corresponding member of the source object.

Comments are closed.

Skip to main content