Using std::unique_ptr and lambdas to advance a state of an object

387 views Asked by At

When advancing the state of an object, use of std::swap works well for simple objects and pointer swaps. For other in place actions, Boost.ScopeExit works rather well, but it's not terribly elegant if you want to share exit handlers across functions. Is there a C++11 native way to accomplish something similar to Boost.ScopeExit but allow for better code reuse?

1

There are 1 answers

0
Sean On BEST ANSWER

(Ab)use std::unique_ptr's custom Deleters as a ScopeExitVisitor or Post Condition. Scroll down to ~7th line of main() to see how this is actually used at the call site. The following example allows for either std::function or lambdas for Deleter/ScopeExitVisitor's that don't require any parameters, and a nested class if you do need to pass a parameter to the Deleter/ScopeExitVisitor.

#include <iostream>
#include <memory>

class A {
 public:
  using Type = A;
  using Ptr = Type*;
  using ScopeExitVisitorFunc = std::function<void(Ptr)>;
  using ScopeExitVisitor = std::unique_ptr<Type, ScopeExitVisitorFunc>;

  // Deleters that can change A's private members. Note: Even though these
  // are used as std::unique_ptr<> Deleters, these Deleters don't delete
  // since they are merely visitors and the unique_ptr calling this Deleter
  // doesn't actually own the object (hence the label ScopeExitVisitor).
  static void ScopeExitVisitorVar1(Ptr aPtr) {
    std::cout << "Mutating " << aPtr << ".var1. Before: " << aPtr->var1;
    ++aPtr->var1;
    std::cout << ", after: " << aPtr->var1 << "\n";
  }

  // ScopeExitVisitor accessing var2_, a private member.
  static void ScopeExitVisitorVar2(Ptr aPtr) {
    std::cout << "Mutating " << aPtr << ".var2. Before: " << aPtr->var2_;
    ++aPtr->var2_;
    std::cout << ", after: " << aPtr->var2_ << "\n";
  }

  int var1 = 10;
  int var2() const { return var2_; }

  // Forward declare a class used as a closure to forward Deleter parameters
  class ScopeExitVisitorParamVar2;

 private:
  int var2_ = 20;
};

// Define ScopeExitVisitor closure. Note: closures nested inside of class A
// still have access to private variables contained inside of A.
class A::ScopeExitVisitorParamVar2 {
 public:
  ScopeExitVisitorParamVar2(int incr) : incr_{incr} {}
  void operator()(Ptr aPtr) {
    std::cout << "Mutating " << aPtr << ".var2 by " << incr_ << ". Before: " << aPtr->var2_;
    aPtr->var2_ += incr_;
    std::cout << ", after: " << aPtr->var2_ << "\n";
  }

 private:
  int incr_ = 0;
};

// Can also use lambdas, but in this case, you can't access private
// variables.
//
static auto changeStateVar1Handler = [](A::Ptr aPtr) {
  std::cout << "Mutating " << aPtr << ".var1 " << aPtr->var1 << " before\n";
  aPtr->var1 += 2;
};

int main() {
  A a;

  std::cout << "a: " << &a << "\n";

  std::cout << "a.var1: " << a.var1 << "\n";
  std::cout << "a.var2: " << a.var2() << "\n";

  { // Limit scope of the unique_ptr handlers. The stack is unwound in
    // reverse order (i.e. Deleter var2 is executed before var1's Deleter).
    A::ScopeExitVisitor scopeExitVisitorVar1(nullptr, A::ScopeExitVisitorVar1);
    A::ScopeExitVisitor scopeExitVisitorVar1Lambda(&a, changeStateVar1Handler);
    A::ScopeExitVisitor scopeExitVisitorVar2(&a, A::ScopeExitVisitorVar2);
    A::ScopeExitVisitor scopeExitVisitorVar2Param(nullptr, A::ScopeExitVisitorParamVar2(5));

    // Based on the control of a function and required set of ScopeExitVisitors that
    // need to fire use release() or reset() to control which visitors are used.
    // Imagine unwinding a failed but complex API call.
    scopeExitVisitorVar1.reset(&a);
    scopeExitVisitorVar2.release(); // Initialized in ctor. Use release() before reset().
    scopeExitVisitorVar2.reset(&a);
    scopeExitVisitorVar2Param.reset(&a);

    std::cout << "a.var1: " << a.var1 << "\n";
    std::cout << "a.var2: " << a.var2() << "\n";
    std::cout << "a.var2: " << a.var2() << "\n";
  }

  std::cout << "a.var1: " << a.var1 << "\n";
  std::cout << "a.var2: " << a.var2() << "\n";
}

Which produces:

a: 0x7fff5ebfc280
a.var1: 10
a.var2: 20
a.var1: 10
a.var2: 20
a.var2: 20
Mutating 0x7fff5ebfc280.var2 by 5. Before: 20, after: 25
Mutating 0x7fff5ebfc280.var2. Before: 25, after: 26
Mutating 0x7fff5ebfc280.var1 10 before
Mutating 0x7fff5ebfc280.var1. Before: 12, after: 13
a.var1: 13
a.var2: 26

On the plus side, this trick is nice because:

  • Code used in the Deleters can access private variables
  • Deleter code is able to be centralized
  • Using lambdas is still possible, though they can only access pubic members.
  • Parameters can be passed to the Deleter via nested classes acting as closures
  • Not all std::unique_ptr instances need to have an object assigned to them (e.g. it's perfectly acceptable to leave unneeded Deleters set to nullptr)
  • Changing behavior at runtime is simply a matter of calling reset() or release()
  • Based on the way you build your stack it's possible at compile time to change the safety guarantees on an object when the scope of the std::unique_ptr(s) go out of scope

Lastly, using Boost.ScopeExit you can forward calls to a helper function or use a conditional similar to what the Boost.ScopeExit docs suggest with bool commit = ...;. Something similar to:

#include <iostream>
#include <boost/scope_exit.hpp>

int main() {
  bool commitVar1 = false;
  bool commitVar2 = false;
  BOOST_SCOPE_EXIT_ALL(&) {
    if (commitVar1)
      std::cout << "Committing var1\n"
    if (commitVar2)
      std::cout << "Committing var2\n"
  };
  commitVar1 = true;
}

and there's nothing wrong with that, but like was asked in the original question, how do you share code without proxying the call someplace else? Use std::unique_ptr's Deleters as ScopeExitVisitors.