RAII and deduced template arguments
Asked Answered
S

2

8

Here's an issue I often run into with RAII. I was wondering if anyone had a good solution for it.

Start with your standard RAII utility class:

class RAIIHelper {
  RAIIHelper() {
    AcquireAResource();
  }
  ~RAIIHelper() {
    ReleaseTheResource();
  }
};

Now, for various reasons, I need to make it a template. Let's also say its constructor takes an argument of the template parameter type:

template <typename T>
class RAIIHelper {
  RAIIHelper(T arg) {
    AcquireAResource();
  }
  ~RAIIHelper() {
    ReleaseTheResource();
  }
};

Now consider a use site:

void func() {
  RAIIHelper<SomeType> helper(someObj);
}

It's annoying to have to write out SomeType when it can be deduced from someObj, so I write a helper function to deduce the type:

template <typename T>
RAIIHelper<T> makeRAIIHelper(T arg) {
  return RAIIHelper<T>(arg);
}

Now I can use it like so:

void func() {
  auto helper = makeRAIIHelper(someObj);
}

Wonderful, right? Except there's a snag: RAIIHelper is now required to be copyable or movable, and the destructor - which releases the resource - can potentially be called twice: once for the temporary returned by makeRAIIHelper, and once for the local variable in the calling function.

In practice, my compiler performs RVO and the destructor is called only once. However, this is not guaranteed. This can be seen from the fact that if I try to give RAIIHelper a = delete'd move constructor, the code no longer compiles.

I could add additional state to RAIIHelper so that it knows not to call ReleaseTheResource() after it's been moved-from, but that's extra work that was unnecessary before I added makeRAIIHelper() to get the type deduction.

Is there a way I can get the type deduction, without having to add extra state to RAIIHelper?

Sentimentalize answered 14/7, 2014 at 18:30 Comment(7)
Beside it looking like you are reinventing unique_ptr there, are you sure there's absolutely no natural sentinel object of type SomeType?Karisa
You can use a unique_ptr.Quince
Write a proper move constructor and disable the copy constructor. This shouldn't be a problem.Coquette
@KerrekSB Why disable the copy ctor? Edit: not sure if one can conclude from the OP that it's brokenGaynellegayner
The fact that everyone is still writing std::lock_guard<std::mutex> lock(m_mutex); is evidence that you can't do this without a type that's either movable or copyable.Streeto
@T.C.: I think there's more to it than that. You could have a deducing function for unique_lock, say. But it's dangerous: { auto x = make_lock(mx_); /* ... */ } is OK, but it's easy to write the erroneous { make_lock(mx_); /* ... */ } instead.Coquette
@KerrekSB: There's a reason GCC has __attribute__((warn_unused_result)) and MS has _Check_return_.Karisa
P
8

There is a very simple solution: Use a reference to the temporary object instead of copying it into a local variable.

void func()
{
    auto&& helper = makeRAIIHelper(someObj);
}
Planarian answered 14/7, 2014 at 18:42 Comment(9)
And if you replace return RAIIHelper<T>(arg); with "direct-initialization" à la return {arg}, the type doesn't need to be movable.Gaynellegayner
How will that work on the next line when the temporary object goes out of scope?Geometrize
@StianV.Svedenborg The lifetime of that temporary is extended until the end of the scope.Gaynellegayner
@dyp: Thanks, that's important to mention.Planarian
@dyp: Are you sure it's guaranteed that no temporary object is created?Coquette
@KerrekSB Is is copy-list-initialization. See https://mcmap.net/q/357864/-can-we-return-objects-having-a-deleted-private-copy-move-constructor-by-value-from-a-function and [stmt.return]/2Gaynellegayner
@dyp: Hm, doesn't "copy"-list-initialization sound like it would be allowed to have a copy? Like X x = { a, b, c };: that one requires X x = X { a, b, c }; to be well-formed, non?Coquette
@KerrekSB IIRC copy-list-initialization is the same as direct-list-initialization, except that the former will not work with an explicit constructor.Township
@dyp: Bummer that my code needs to compile with gcc 4.4, which doesn't support return {arg} yet. Otherwise this solution is perfect!Sentimentalize
G
1

Building on the previous answers and comments:

You could leave the responsibility of being movable to unique_ptr, and return your resource like this:

template <class T>
auto makeRAII( T arg ) -> std::unique_ptr<RAIIHelper> {
     return make_unique(RAIIHelper<T>(arg));
}

Now it scopes like a static variable, but may be uncopyable & unmovable.

Geometrize answered 14/7, 2014 at 18:53 Comment(2)
That works, but it imposes the overhead of an unnecessary dynamic allocation.Sentimentalize
Aye, which may or may not be worth it, depending on the lifetime of the object and the lazyness (which is a good thing) of the developer.Geometrize

© 2022 - 2024 — McMap. All rights reserved.