Search code examples
c++templatesc++17decorator

generic decorators for callable objects with conditional return


I want to write decorator functions for callable objects.

This is what I have now:

#include <utility>

template <typename DecoratedT, typename CallableT>
constexpr auto before_callable(DecoratedT &&decorated, CallableT &&callBefore)
{
    return [decorated = std::forward<DecoratedT>(decorated),
            callBefore = std::forward<CallableT>(callBefore)](auto &&...args){

                callBefore(std::as_const(args)...); // TODO: ignore parameters?
                auto &&res = decorated(std::forward<decltype(args)>(args)...);
                return res;
                };
}

template <typename DecoratedT, typename CallableT>
constexpr auto after_callable(DecoratedT &&decorated, CallableT &&callAfter)
{
    return [decorated = std::forward<DecoratedT>(decorated),
            callAfter = std::forward<CallableT>(callAfter)](auto &&...args){
                auto &&res = decorated(std::forward<decltype(args)>(args)...);
                callAfter(std::as_const(args)...); // TODO: ignore parameters?

                return res;
    };
}

template <typename DecoratedT, typename CallBeforeT, typename CallAfterT>
constexpr auto decorate_callable(DecoratedT &&decorated, 
                                CallBeforeT &&callBefore, 
                                CallAfterT &&callAfter)
{
    return before_callable(after_callable(std::forward<DecoratedT>(decorated), 
                                          std::forward<CallAfterT>(callAfter)),
                            std::forward<CallBeforeT>(callBefore));
}

This code works while a decorated object has a return type which is not void. Error otherwise:

<source>:21:24: error: forming reference to void
   21 |                 auto &&res = decorated(std::forward<decltype(args)>(args)...);
      |                        ^~~
#include <iostream>

template <typename SumT>
void print(const SumT& sum)
{
    const auto &res = sum(4, 811);
    std::cout << res << std::endl;
}

int main()
{
    struct {
        int operator()(int lhs, int rhs) const
        {
            std::cout << "summing\n"; 
            return lhs + rhs;
        }
    } sum{};

    const auto &printBefore = [](const int lhs, const int rhs, const auto &...){
        std::cout << "Before sum (args): " << lhs << " " << rhs << std::endl;
    };

    const auto &printAfter = [](const auto &...){
        std::cout << "After sum" << std::endl;
    };

    std::cout << "Undecorated: ";
    print(sum);
    std::cout << "\nDecorated Before:\n";
    print(before_callable(sum, printBefore));

    std::cout << "\nDecorated After:\n";
    print(after_callable(sum, printAfter));

    std::cout << "\nDecorated Before and After:\n";
    print(decorate_callable(sum, printBefore, printAfter));

    struct {
        void operator()() const {}
    } retVoid{};

    const auto &voidDecorated = decorate_callable(retVoid, 
                                    [](const auto &...){}, 
                                    [](const auto &...){});
    // voidDecorated(); // does not compile

    return 0;
}

https://godbolt.org/z/x94ehTq17

  1. What is a simple and optimization-friendly way of handling void return type from a decorated object?
  2. How can I optionally ignore arguments for lambdas where I don't need them? Using variadic lambdas is a one way, but it is enforced on the user side.
  3. Should I use decltype(auto) for return in function declarations?

Solution

  • I'm using the SCOPE_EXIT macro from Andrei Alexandrescus talk about Declarative Control Flow. The trick here is that SCOPE_EXIT creates an object with a lambda (the following block) and executes the lambda in the destructor. This delays the execution until the control flow exits the block.

    SCOPE_EXIT will always execute the code, SCOPE_SUCCESS will only execute the code if no exception is thrown and SCOPE_FAIL will execute the code only when an exception is thrown.

    Note: the original decorator didn't execute the callAfter when an exception is thrown.

    #include <utility>
    #include <exception>
    
    #ifndef CONCATENATE_IMPL
    #define CONCATENATE_IMPL(s1, s2) s1##s2
    #endif
    #ifndef CONCATENATE
    #define CONCATENATE(s1, s2) CONCATENATE_IMPL(s1, s2)
    #endif
    
    #ifndef ANONYMOUS_VARIABLE
    #ifdef __COUNTER__
    #define ANONYMOUS_VARIABLE(str) CONCATENATE(str, __COUNTER__)
    #else
    #define ANONYMOUS_VARIABLE(str) CONCATENATE(str, __LINE__)
    #endif
    #endif
    
    template <typename FunctionType>
    class ScopeGuard {
      FunctionType function_;
    public:
      explicit ScopeGuard(const FunctionType& fn) : function_(fn) { }
      explicit ScopeGuard(const FunctionType&& fn) : function_(std::move(fn)) { }
      ~ScopeGuard() noexcept {
        function_();
      }
    };
    
    enum class ScopeGuardOnExit { };
    
    template <typename Fun>
    ScopeGuard<Fun> operator +(ScopeGuardOnExit, Fun&& fn) {
      return ScopeGuard<Fun>(std::forward<Fun>(fn));
    }
    #define SCOPE_EXIT \
      auto ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE) \
      = ScopeGuardOnExit() + [&]()
    
    template <typename DecoratedT, typename CallableT>
    constexpr auto after_callable(DecoratedT &&decorated, CallableT &&callAfter)
    {
        return [decorated = std::forward<DecoratedT>(decorated),
                callAfter = std::forward<CallableT>(callAfter)](auto &&...args){
                    SCOPE_EXIT {
                        callAfter(std::as_const(args)...); // TODO: ignore parameters?
                    };
                    auto &&res = decorated(std::forward<decltype(args)>(args)...);
    
                    return res;
        };
    }