14

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);)
Chris
  • 26,361
  • 5
  • 21
  • 42
tauran
  • 7,986
  • 6
  • 41
  • 48
  • 3
    Google for `ScopeGuard` (ignore the code project link an look for the drdobbs article) alternatively use `shared_ptr` with a custom deleter.... – David Rodríguez - dribeas Jul 15 '12 at 13:19
  • @tauran: What's the advantage over automatically called destructors? –  Jul 15 '12 at 16:42
  • 8
    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. – Konrad Rudolph Jul 15 '12 at 17:11
  • 1
    @David: I'd rather recommend `unique_ptr` over `shared_ptr`, though. – Xeo Jul 15 '12 at 17:16
  • @Konrad: I'd state more accurately that he clearly knows *of* them, but not much *about* them. – Puppy Jul 15 '12 at 17:17
  • @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. – David Rodríguez - dribeas Jul 15 '12 at 17:24
  • @David: You can use `decltype` for that, although it's not as clean. – Puppy Jul 15 '12 at 17:27
  • @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. – David Rodríguez - dribeas Jul 15 '12 at 17:31
  • @David: http://ideone.com/ckbaM seems to work fine. – Puppy Jul 15 '12 at 17:35
  • @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 :) – David Rodríguez - dribeas Jul 15 '12 at 17:39
  • @David: `auto up(make_unique<...>([](...){...}));`. – Xeo Jul 15 '12 at 17:42
  • @KonradRudolph makes sense :) Maybe you give it a try, would be funny to have my own code on DailyWTF. – tauran Jul 15 '12 at 18:50
  • @Xeo: That is the first thing I mentioned: a template to create the `unique_ptr`: *You **can** use an intermediate template and `auto`*. – David Rodríguez - dribeas Jul 15 '12 at 23:18
  • @David: Oh, sorry, I didn't really read that as such. I somehow... thought of something else. – Xeo Jul 15 '12 at 23:21
  • @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. – David Rodríguez - dribeas Jul 15 '12 at 23:34
  • @David: I personally would implement deleter arguments the same way that allocator arguments get passed in variadic templates, with a marker: http://ideone.com/fT4pQ. – Xeo Jul 15 '12 at 23:52
  • 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: http://codereview.stackexchange.com/questions/16328/improve-my-little-syntactic-hack – Mike C Oct 08 '12 at 19:38

4 Answers4

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    
David Lawson
  • 7,802
  • 4
  • 31
  • 37
Johan Lundberg
  • 26,184
  • 12
  • 71
  • 97
  • 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 :) – tauran Jul 15 '12 at 18:39
  • 1
    Right, I guessed that you knew but I wanted to make the answer complete. – Johan Lundberg Jul 15 '12 at 18:52
  • 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. – Banjocat Dec 11 '15 at 02:02
  • 7
    @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. – Johan Lundberg Dec 12 '15 at 09:25
  • @johanlundberg hmm. I like that. I surrender. – Banjocat Dec 13 '15 at 17:52
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;
}
Puppy
  • 144,682
  • 38
  • 256
  • 465
  • 5
    Your code introduces a thoroughly pointless additional class. – Puppy Jul 15 '12 at 17:21
  • 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. – Johan Lundberg Jul 15 '12 at 17:22
  • 1
    Hmm. Possibly I misread the question- he was not very specific about what the *problem* was. – Puppy Jul 15 '12 at 17:26
  • Yes, an utterly overdone original solution from OP in any case. – Johan Lundberg Jul 15 '12 at 17:28
  • 3
    @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. – David Rodríguez - dribeas Jul 15 '12 at 17:29
  • 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. – Puppy Jul 15 '12 at 17:36
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
});
David Lawson
  • 7,802
  • 4
  • 31
  • 37
1

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 ...
    } }
}
tinkerbeast
  • 1,707
  • 1
  • 20
  • 42
  • 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)` – Dwayne Robinson Jun 03 '23 at 01:27