Python with statement in C++
Asked Answered
P

4

16

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);)
Pictograph answered 15/7, 2012 at 11:31 Comment(17)
Google for ScopeGuard (ignore the code project link an look for the drdobbs article) alternatively use shared_ptr with a custom deleter....Nobile
@tauran: What's the advantage over automatically called destructors?Vardhamana
Don’t take this the wrong way, since you clearly know your C++ (as evidenced from the (technically) correct use of virtual functions, templates and type packs) but this belongs on the DailyWTF: you are trying, with considerable effort and complexity, to emulate a feature from another language which itself is an inferior emulation of a feature that C++ has natively.Lasting
@David: I'd rather recommend unique_ptr over shared_ptr, though.Serpentine
@Konrad: I'd state more accurately that he clearly knows of them, but not much about them.Bistort
@Xeo: For the general solution (i.e. other than memory management) the interface to control deleters in shared_ptr is much nicer than in unique_ptr as in the former the deleter is effectively type-erased from the type, allowing the use of, for example, lambda expressions whose names are unutterable.Nobile
@David: You can use decltype for that, although it's not as clean.Bistort
@DeadMG: Try it if you want :), You can use an intermediate template and auto, but I don't think you can use decltype to solve the problem.Nobile
@David: ideone.com/ckbaM seems to work fine.Bistort
@DeadMG: Yes, I thought of that and was verifying the properties of lambdas while you worked the example. At any rate, you need to create the lambda variable in a different expression, which has the side effects of a) making the user more cumbersome, b) injecting a new variable (the lambda) to the scope. I still consider this as not a nice interface :)Nobile
@David: auto up(make_unique<...>([](...){...}));.Serpentine
@KonradRudolph makes sense :) Maybe you give it a try, would be funny to have my own code on DailyWTF.Pictograph
@Xeo: That is the first thing I mentioned: a template to create the unique_ptr: You can use an intermediate template and auto.Nobile
@David: Oh, sorry, I didn't really read that as such. I somehow... thought of something else.Serpentine
@Xeo: Note that make_unique is not part of the standard, and it is not trivial to design/implement it correctly (where would you pass the deleter?). As a matter of fact, std::make_shared does not have support for passing a deleter either (in the case of std::make_shared the problem is yet a tad more complex than it would be for make_unique, as the latter does not need to manage the count object.Nobile
@David: I personally would implement deleter arguments the same way that allocator arguments get passed in variadic templates, with a marker: ideone.com/fT4pQ.Serpentine
I imagine this will be even less acceptable than tauran's, but I tried a similar solution, not to avoid the RAII aspect but to avoid the awkward introduce-new-scope-without-keyword aspect: codereview.stackexchange.com/questions/16328/…Interatomic
W
14

The with statement is a way to do in python what is already the normal thing in C++. It is called RAII: Resource acquisition is initialization.

In python, when a class object is created, the __init__ method is called (but this is not a strict guarantee). The __del__ method is called by the garbage collector at some point after the object is no longer in use, but it is not deterministic.

In C++ the destructor is called at a well defined point so there is no need for with.

I suggest you just use something like class B (no need for class A or With).

template <typename T>
class B {
public:
    B(T& t) : m_t(t){
        m_t.bind();
    }
    ~B() {
        m_t.release();
    }
    T& m_t;
}

use it like this:

{
    B<X> bound_x(x);  // x.bind is called
    B<Y> bound_y(y);  // y.bind is called
    // use x and y here
} // bound_x and bound_y is destroyed here 
  // so x.release and y.release is called    
Wyck answered 15/7, 2012 at 16:59 Comment(5)
I know that. I used RAII too -> Class B. I just wanted to wrap it in one statement (without macros). But from the responses here I think the average dev prefers your solution and therefore it's better than mine :)Pictograph
Right, I guessed that you knew but I wanted to make the answer complete.Wyck
It isn't the same. Python allows you to use a 'with' statement in the middle of a block of code. It allows you to get a resource and release it then do some other stuff. C++ RAII requires that releasing is always done at the end. This is not always ideal. 'with' is more versatile than RAII.Foltz
@Banjocat, Python with is really similar to an opening and closing brace in C++. If you would like the resource to be obtained and released in the middle of a method, then just open up a new scope there. The scope need not be the full function or method.Wyck
@johanlundberg hmm. I like that. I surrender.Foltz
B
2

It ships with the language, and it's called RAII.

struct X {
    X() { std::cout << "bind\n"; }
    ~X() { std::cout << "release\n"; }
};
int main() {
    X x;
}
Bistort answered 15/7, 2012 at 17:16 Comment(6)
Your code introduces a thoroughly pointless additional class.Bistort
well, he needs help calling .bind() and .release() which presumably exist in the Qt-OpenGL classes he is using. And your answer does not do that. At least that's how I interpreted the question. Seems like bad design, I agree, but that seems to be out of his control.Wyck
Hmm. Possibly I misread the question- he was not very specific about what the problem was.Bistort
Yes, an utterly overdone original solution from OP in any case.Wyck
@DeadMG: your code does not introduce a class, but assumes that the type on which bind and release are to be called can be modified to add the logic which might not be the case.Nobile
Well, it's in a sense irrelevant. If you have logic which must be called in pairs like that, then you must write an RAII-ified wrapper in any case, realistically.Bistort
J
2

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 ...
    } }
}
June answered 2/7, 2022 at 19:50 Comment(1)
I did something similar. The with macro is sad, but it makes the code much cleaner at the call sites, wherever you lock a mutex or recurse a GUI true. #define with(exp) if (auto t = (exp).Enter(); true)Smallsword
D
1

One possible solution:

template <typename T>
void with(T *t, std::function<void ()> fn) {
    t->bind();
    fn();
    t->unbind();
}

Usage:

with(object, []() {
    // object bound
});
Dynamometer answered 11/4, 2018 at 13:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.