3

I am on the journey to make a python like decorator with the latest available C++ techniques. I have seen some solution already here (Python-like C++ decorators), but I wonder if it can be done better. With help from others (Constructing std::function argument from lambda) I came up with the following solution.

template<typename TWrapped>
auto DurationAssertDecorator(const std::chrono::high_resolution_clock::duration& maxDuration, TWrapped&& wrapped)
{
    return [wrapped = std::forward<TWrapped>(wrapped), maxDuration](auto&&... args)
    {
        const auto startTimePoint = std::chrono::high_resolution_clock::now();

        static_assert(std::is_invocable<TWrapped, decltype(args)...>::value, "Wrapped object must be invocable");

        if constexpr (!(std::is_void<decltype(wrapped(std::forward<decltype(args)>(args)...))>::value))
        {
            // return by reference will be here not converted to return by value?
            //auto result = wrapped(std::forward<decltype(args)>(args)...);

            decltype(wrapped(std::forward<decltype(args)>(args)...)) result = wrapped(std::forward<decltype(args)>(args)...);

            const auto endTimePoint = std::chrono::high_resolution_clock::now();
            const auto callDuration = endTimePoint - startTimePoint;
            assert(callDuration <= maxDuration);

            return result;
        }
        else
        {
            wrapped(std::forward<decltype(args)>(args)...);

            const auto endTimePoint = std::chrono::high_resolution_clock::now();
            const auto callDuration = endTimePoint - startTimePoint;
            assert(callDuration <= maxDuration);
        }
    };
}

I do not use below "auto" on purpose to make sure that the return type is what I expect (or at least compatible).

I shall be able use it with any callable: stateless lambda, statefull lambda, struct functor, function pointer, std::function

std::function<double(double)> decorated = DurationAssertDecorator(1s, [](const double temperature) { return temperature + 5.0; });
double a = decorated (4);

Composition shall be OK, too:

std::function<double()> wrapped = LogDecorator(logger, [] { return 4.0; });
std::function<double()> wrapped_wrapped = DurationAssertDecorator(1s, functor);

This shall be not OK - int literal 5 is not a callable:

std::function<void(double)> decorated = DurationAssertDecorator(1s, 5);

So far it does the trick, however:

  • The case - where the wrapped function has a return value - I was not sure if I just get the result by auto and the return value of the wrapped is a reference. If so then a copy will take place instead keeping the reference (return by pointer and by value shall be OK). So that is why I came up with that strange construct. Can I do it better?
  • What other improvements/fixes are possible?
user2281723
  • 519
  • 1
  • 5
  • 16
  • Are you sure about moving from `wrapped` in lambda capture? Seems like `std::forward` might be a better way to go. – paler123 Feb 14 '19 at 14:32
  • Thanks, I think you are right: forward shall work with normal and move references. I have adapted the code in the questrion – user2281723 Feb 14 '19 at 15:08

1 Answers1

0

I have realized, that I can simplify the code much more if I use a RAII object for the pre- and post- call activities. The void and non-void return value handling is not necessary any more.

template<typename TWrapped>
auto DurationAssertDecorator(const std::chrono::high_resolution_clock::duration& maxDuration, TWrapped&& wrapped)
{
    return [wrapped = std::forward<TWrapped>(wrapped), maxDuration](auto&&... args) mutable
    {
        static_assert(std::is_invocable<TWrapped, decltype(args)...>::value, "Wrapped object must be invocable");

        struct Aspect
        {
            // Precall logic goes into the constructor
            Aspect(const std::chrono::high_resolution_clock::duration& maxDuration)
                : _startTimePoint(std::chrono::high_resolution_clock::now())
                , _maxDuration(maxDuration)
            {}

            // Postcall logic goes into the destructor
            ~Aspect()
            {
                const auto endTimePoint = std::chrono::high_resolution_clock::now();
                const auto callDuration = endTimePoint - _startTimePoint;
                assert(callDuration <= _maxDuration);
            }

            const std::chrono::high_resolution_clock::time_point _startTimePoint;
            const std::chrono::high_resolution_clock::duration& _maxDuration;
        } aspect(maxDuration);

        return wrapped(std::forward<decltype(args)>(args)...);
    };
}

It works with the normal use-case:

auto wrappedFunctor = DurationAssertDecorator(1s, [](const double temperature)  { return temperature; });

I wanted also to work it with non-const functors, like mutable lambdas:

auto wrappedFunctor = DurationAssertDecorator(1s, 
    [firstCall = true](const double temperature) mutable
    {
        if (firstCall)
        {
            firstCall = false;
            return temperature;
        }
        std::this_thread::sleep_for(2s);
        return temperature;
    });

So I am quite happy with this solution.

user2281723
  • 519
  • 1
  • 5
  • 16