Python with statement in C++

Viewed 2663

I am trying to implement something similar to the Python with statement in C++. As I plan to use it mainly with Qt-OpenGL the methods are called bind and release (in Python __enter__, __exit__).

Code I came up with:

header:

#include <iostream>
#include <vector>

class With
{
public:
    class A
    {
    public:
        virtual ~A() { }
    };

    template <typename T>
    class B : public A
    {
    public:
        B(T& _t) : t(_t)
        {
            t.bind();
        }

        virtual ~B()
        {
            t.release();
        }

        T& t;
    };

    template <typename... Args>
    With(Args&... args)
    {
        set(args...);
    }

    ~With();

    template <typename T, typename... Args>
    void set(T& t, Args&... args)
    {
        set(t);
        set(args...);
    }

    template <typename T>
    void set(T& t)
    {
        a.push_back(dynamic_cast<A*>(new B<T>(t)));
    }

    std::vector<A*> a;
};

cpp:

With::~With()
{
    for (auto it = a.begin(); it != a.end(); ++it)
    {
        delete *it;
    }
}

Usage:

class X
{
public:
    void bind() { std::cout << "bind x" << std::endl; }
    void release() { std::cout << "release x" << std::endl; }
};

class Y
{
public:
    void bind() { std::cout << "bind y" << std::endl; }
    void release() { std::cout << "release y" << std::endl; }
};

int main()
{
    X y;
    Y y;

    std::cout << "start" << std::endl;
    {
        With w(x, y);
        std::cout << "with" << std::endl;
    }

    std::cout << "done" << std::endl;

    return 0;
}

Questions:

  1. Needing class A and class B feels a bit clumsy. Is there a better alternative?
  2. Are there any draw backs in using && instead of &? It would make the usage of tempory objects possible (e.g. With w(X(), y);)
4 Answers

While I agree with the answers here that RAII is the C++ way to manage resource life-cycles, I personally find RAII not robust enough to handle all scenarios. I find that the constructor/destructor paradigm is good enough for memory resource management. For other resources (eg. file descriptors, sockets, timers etc) using an explicit resource management block provides more robustness. My reasoning is as follows -

  • Assume your resource is a file descriptor. You want to close it in the destructor but some IO error occurred. What do you do? Throwing an exception from a destructor is a very bad choice. Just ignoring the error is probably just as bad. For situations like this other languages give resource block mechanism to decouple resource management from object lifecycle.
  • C++ already faces this issue in its standard library. The std::mutex lock and unlock methods are essentially resource holders. However the lifecycle of std::mutex cannot be tied to lock/unlock. So we have a wrapper class std::lock_guard solely for lock/unlock RAII purpose. While an elegant solution, the problem is this lacks generality. Imagine every resource-management related class coming up with its own wrapper class. If we had a standard defined resource-management mechanism we could probably uniformly do this with a single wrapper template.

I hope this convinces RAII proponents that a standard specified resource-management model on top of RAII paradigm would be good.

Coming to the question about Python with clause: A very naive implementation would be something as follows:

// Standardised interface for resource managers.
template<typename T>
concept Withable = requires(T t) {
    { t.bind() } -> std::same_as<void>;
    { t.release() } -> std::same_as<void>;
};

// Universal wrapper for all resource managers.
template<Withable T>
struct WithWrapper {
    T* ref_;

    WithWrapper(T& obj)
    : ref_{&obj}
    { ref_->bind(); }

    ~WithWrapper() { ref_->release(); }

    T& get() { return *ref_; }
};

// In case we want the with keyword.
#define with(...) if (__VA_ARGS__; true)

Usage for your case would be -

// Making sure your types are compatible.
static_assert(Withable<X>);
static_assert(Withable<Y>);

// Using the with and wrapper.
int main() {
    X y;
    Y y;
    // With my naive implementation it's not really possible to declare two
    // separate types in same initialisation block. However even with two 
    // "with"s, the execution behaviour is extacly the same as Python.
    with (auto xw = WithWrapper(x)) { with (auto yw = WithWrapper(y)) {
        // Do something with xw and yw ...
    } }
}
Related