patterncppModerate
On-the-fly destructors
Viewed 0 times
destructorsthefly
Problem
Due to my needing to use C libraries inside C++, I kept finding myself wanting to be able to ensure that the
utils.hpp
This can be used like so:
Outputs:
free function got called, even if an exception got thrown. UTILS_SCOPE_EXIT ensures that the function will be called at the end of scope. External documentation shows how to use it, and also clarifies that it is not allowed to have multiple calls on the same line (because __COUNTER__ is not always supported):utils.hpp
#pragma once
#include
namespace utils { namespace internal {
class ScopeExit_ {
public:
ScopeExit_(std::function callback)
: callback_{ callback }
{}
~ScopeExit_()
{
callback_();
}
private:
std::function callback_;
};
#define UTILS_INTERNAL_CAT_HELPER(x, y) x ## y
#define UTILS_INTERNAL_CAT(x, y) UTILS_INTERNAL_CAT_HELPER(x, y)
#define UTILS_SCOPE_EXIT(...) utils::internal::ScopeExit_ UTILS_INTERNAL_CAT(utils_internal_ScopeExit_, __LINE__){ __VA_ARGS__ }
} }This can be used like so:
#include
int main()
{
UTILS_SCOPE_EXIT([]{ std::cout << "1" << std::endl; });
UTILS_SCOPE_EXIT([]{ std::cout << "2" << std::endl; });
UTILS_SCOPE_EXIT([]{ std::cout << "3" << std::endl; });
std::cout << "4" << std::endl;
std::cout << "5" << std::endl;
}Outputs:
4
5
3
2
1Solution
Efficiency
The main issue with
See: http://coliru.stacked-crooked.com/a/9503ca27f1faded8
Solution
Don't use
A lightweight implementation
I promise you that we can reduce the on-scope-exit object's size to a tiny 1 byte when all we want to do is take an action at the end of a scope by using lambdas and templates.
Note: The implementation requires the `
The main issue with
std::function is that it is heavy in memory usage. It comes in at a minimum of 32 bytes in G++ and (if memory serves me right) 40 bytes in VC++2015 32-bit.See: http://coliru.stacked-crooked.com/a/9503ca27f1faded8
Solution
Don't use
std::function.A lightweight implementation
I promise you that we can reduce the on-scope-exit object's size to a tiny 1 byte when all we want to do is take an action at the end of a scope by using lambdas and templates.
Note: The implementation requires the `
and headers.
Step 1: Make a helper function that creates a lambda that calls a callable object.
template
auto make_lambda( F&& f, Args&&... f_args ) noexcept
{
// important to capture by copy to prevent argument lifetime issues
return [=] () mutable -> std::result_of_t
{
return std::forward( f )( std::forward( args )... );
};
}
- This function creates a lambda that calls the argument callable. We do this so that we can store it for later use; for our use case, in a destructor so that it is called at the end of a scope.
- As of C++14, the
noexcept specifier is not part of a function's type, so when C++17 comes around and changes that, you'll probably want to come back to this function. See this for more information: https://stackoverflow.com/q/33589752/2296177
Step 2: A function to generate the object that calls the stored lambda in its destructor.
template
auto on_scope_exit( F&& f ) noexcept
{
class unique_scope_exit_t final
{
private:
using fn_t = decltype( make_lambda( std::move( f ) ) );
fn_t fn_;
public:
~unique_scope_exit_t()
noexcept( noexcept( fn_() ) )
{
fn_(); // call it in our destructor
}
unique_scope_exit_t( F&& fn ) // can be extended for complex lambdas
noexcept( std::is_nothrow_move_constructible::value )
: fn_{ make_lambda( std::move( fn ) ) } // take ownership and store the lambda
{}
unique_scope_exit_t( unique_scope_exit_t&& rhs )
noexcept( std::is_nothrow_move_constructible::value )
: fn_{ std::move( rhs.fn_ ) }
{}
unique_scope_exit_t( unique_scope_exit_t const& ) = delete;
unique_scope_exit_t& operator=( unique_scope_exit_t const& ) = delete;
unique_scope_exit_t& operator=( unique_scope_exit_t&& ) = delete;
};
return unique_scope_exit_t{ std::move( f ) }; // we take ownership of argument 'f'
}
- Having a class definition that is local to our function allows us to use the deduced type of the template type argument.
- Take advantage of the
header to make it optimal by declaring everything noexcept when possible.
- You can provide a variadic template to the constructor in order to make full usage of
make_lambda() and so that you can support complex end-of-scope actions. I leave this implementation to you, it is trivial.
- The C++17 standard will guarantee copy elision for the function return.
As promised, one of our objects takes a single byte when its lambda doesn't capture anything. That is:
auto test = on_scope_exit( [] { std::cout << "exiting\n"; } );
static_assert( sizeof( decltype( test ) ) == 1, "!" );
This is as it implies... the size of our generated object only takes as many bytes as it needs.
We have achieved an implementation that is both flexible and efficient (in speed and size).
Sample usage
Similar to your usage, however there are no macros involved (personal preference). You can easily wrap this in a macro if it makes you happy.
#include
int main()
{
// remember to store the call's return to prevent it from running early
auto exit1 = on_scope_exit( [] { std::cout << "1\n"; } );
auto exit2 = on_scope_exit( [] { std::cout << "2\n"; } );
auto exit3 = on_scope_exit( [] { std::cout << "3\n"; } );
std::cout << "4\n";
std::cout << "5\n";
}
Full demo: http://coliru.stacked-crooked.com/a/da1afa40ae073523
Due to comments.
Other possible implementations
As noted by Loki Astari and Nikita Kakuev, the previous implementation can increase the executable's size if it is used with many (very many) different types.
In a regular use case, I still think this is a better approach than constantly constructing and destroying 32+ bytes. For example, VC++2015's std::function is 40 bytes in 32-bit mode and 64 bytes in 64-bit mode.
As Nikita Kakuev points out here, make_lambda()'s signature might be over-generalized for this specific case, so it could be omitted in favour of a more succinct on_scope_exit().
The original thought was that you could make unique_scope_exit_t's constructor take a variadic template in order to allow complex end-of-scope actions by storing callables whose operator() have parameters.
If this isn't desired, we can simply remove make_lambda() and provide the following implementation of on_scope_exit()` with a reduced executablCode Snippets
template <typename F, typename... Args>
auto make_lambda( F&& f, Args&&... f_args ) noexcept
{
// important to capture by copy to prevent argument lifetime issues
return [=] () mutable -> std::result_of_t<F( Args... )>
{
return std::forward<F>( f )( std::forward<Args>( args )... );
};
}template<class F>
auto on_scope_exit( F&& f ) noexcept
{
class unique_scope_exit_t final
{
private:
using fn_t = decltype( make_lambda( std::move( f ) ) );
fn_t fn_;
public:
~unique_scope_exit_t()
noexcept( noexcept( fn_() ) )
{
fn_(); // call it in our destructor
}
unique_scope_exit_t( F&& fn ) // can be extended for complex lambdas
noexcept( std::is_nothrow_move_constructible<fn_t>::value )
: fn_{ make_lambda( std::move( fn ) ) } // take ownership and store the lambda
{}
unique_scope_exit_t( unique_scope_exit_t&& rhs )
noexcept( std::is_nothrow_move_constructible<fn_t>::value )
: fn_{ std::move( rhs.fn_ ) }
{}
unique_scope_exit_t( unique_scope_exit_t const& ) = delete;
unique_scope_exit_t& operator=( unique_scope_exit_t const& ) = delete;
unique_scope_exit_t& operator=( unique_scope_exit_t&& ) = delete;
};
return unique_scope_exit_t{ std::move( f ) }; // we take ownership of argument 'f'
}auto test = on_scope_exit( [] { std::cout << "exiting\n"; } );
static_assert( sizeof( decltype( test ) ) == 1, "!" );#include <iostream>
int main()
{
// remember to store the call's return to prevent it from running early
auto exit1 = on_scope_exit( [] { std::cout << "1\n"; } );
auto exit2 = on_scope_exit( [] { std::cout << "2\n"; } );
auto exit3 = on_scope_exit( [] { std::cout << "3\n"; } );
std::cout << "4\n";
std::cout << "5\n";
}template<class F>
auto on_scope_exit( F&& f )
noexcept( std::is_nothrow_move_constructible<F>::value )
{
class unique_scope_exit_t final
{
F f_;
public:
~unique_scope_exit_t()
noexcept( noexcept( f_() ) )
{
f_();
}
explicit unique_scope_exit_t( F&& f )
noexcept( std::is_nothrow_move_constructible<F>::value )
: f_( std::move( f ) )
{}
unique_scope_exit_t( unique_scope_exit_t&& rhs )
noexcept( std::is_nothrow_move_constructible<F>::value )
: f_{ std::move( rhs.f_ ) }
{}
unique_scope_exit_t( unique_scope_exit_t const& ) = delete;
unique_scope_exit_t& operator=( unique_scope_exit_t const& ) = delete;
unique_scope_exit_t& operator=( unique_scope_exit_t&& ) = delete;
};
return unique_scope_exit_t{ std::move( f ) };
}Context
StackExchange Code Review Q#134234, answer score: 17
Revisions (0)
No revisions yet.