HiveBrain v1.2.0
Get Started
← Back to all entries
patterncppModerate

On-the-fly destructors

Submitted by: @import:stackexchange-codereview··
0
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 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
1

Solution

Efficiency

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 executabl

Code 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.