Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Searching for a trick to avoid creating a field in a class template

Tags:

c++

I have a scope guard like class (this is the simplified test case):

template<void(*close)()>
struct Guard1
{
  template<typename O>
  Guard1(O open) { open(); }

  ~Guard1() { close(); }
};

void close() { std::cout << "close g1\n"; }

int main()
{
  Guard1<close> g1 = [](){ std::cout << "open g1\n"; };
}

I modified it such that the close expression can also be given as a lambda:

class Guard2
{
  std::function<void()> close;
public:

  template<typename O, typename C>
  Guard2(O open, C close) : close(close)
  {
    open();
  }

  ~Guard2() { close(); }
};


int main()
{
  Guard2 g2(
      [](){ std::cout << "open g2\n"; },
      [](){ std::cout << "close g2\n"; });
}

However I had to introduce an extra field const std::function<void()>& close; to pass the lambda from the constructor to the destructor.

Is there a way to avoid this extra field while still keeping the lambda (and a nice syntax when used as well)?

like image 529
Micha Wiedenmann Avatar asked Jun 18 '15 13:06

Micha Wiedenmann


3 Answers

Since you want to use it only as ScopeGuard - then you can be sure that const reference or rvalue reference to your close() are valid. You need a member or base class as in other answer - but this is not very big difference. But you can have it as rvalue reference to your lambda, not to std::function which is of quite big performance cost:

template <class Close>
class ScopeGuard {
public:
    template <typename Open>
    ScopeGuard(Open&& open, Close&& close) 
         : close(std::forward<Close>(close))
    {
        open();
    }
    ScopeGuard(ScopeGuard&& other) : close(std::move(other.close))
    {}
    ~ScopeGuard()
    {
        close();
    }
private:
    Close&& close;
};

To make it easier to use - have this make function:

template <class Open, class Close>
auto makeScopeGuard(Open&& open, Close&& close)
{
    return ScopeGuard<Close>(std::forward<Open>(open), 
                             std::forward<Close>(close));
}

And usage:

#include <iostream>
using namespace std;

int main() 
{
   int i = 0;
   auto scope = makeScopeGuard([&i]{cout << "Open " << i++ << "\n";}, 
                               [&i]{cout << "Close " << i++ << "\n";});
   cout << "Body\n";
}

Output:

Open 0
Body
Close 1

I verified it works for gcc and clang, C++14 without errors/warnings.

like image 129
PiotrNycz Avatar answered Oct 24 '22 01:10

PiotrNycz


If you can accept a bit of cheating: stuff the lambda into a base class instead of a field:

#include <iostream>

template<typename Base>
struct Guard1 : public Base
{
  template<typename O>
  Guard1(O open, Base base ) : Base(base) { open(); }
  Guard1(Guard1 const& rhs) : Base(static_cast<Base const&>(rhs)) { }

  ~Guard1() { (*this)(); }
};

template<typename O, typename C>
Guard1<C> makeGuard(O o, C c) { return Guard1<C>(o,c); }

int main()
{

  auto g1 = makeGuard([](){ std::cout << "open g1\n"; },
                      [](){ std::cout << "close g1\n"; } );
}
like image 3
MSalters Avatar answered Oct 24 '22 01:10

MSalters


Is there a way to avoid this extra field while still keeping the lambda (and a nice syntax when used as well)?

Yes: If you observe, there is nothing to be gained by passing the open function to your scope guard (therefore the Single Responsibility Principle states you should not have it there).

You should also pass the function as a runtime parameter, not a template parameter. This will allow for more natural syntax in client code.

You should make the type independent on the template type. This will also make for more natural syntax in client code.

You should ensure the destructor does not throw.

class Guard final
{
public:
    Guard1(std::function<void()> at_scope_exit)
    : f_(std::move(at_scope_exit))
    {
    }

    ~Guard1() noexcept { try{ f_(); } catch(...) {} }
private:
    std::function<void()> f_;
};

Your client code should then look like this:

int x()
{
    operation.begin_transaction();
    Guard commit{ [&operation](){ operation.commit_transaction(); } };

    // do things & stuff here
}
like image 1
utnapistim Avatar answered Oct 24 '22 01:10

utnapistim