Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++17: Wrapping callable using generic variadic lambda

I want to wrap a callable of any type (e.g. a lambda) transparently inside another callable to inject additional functionality. The wrapper's type should have the same characteristics as the original callable:

  • Identical parameter types
  • Identical return type
  • Perfect forwarding of passed arguments
  • Same behaviour when used in SFINAE constructs

I attempted to use generic variadic lambdas as wrappers:

#include <iostream>
#include <type_traits>

template<class TCallable>
auto wrap(TCallable&& callable) {
    return [callable = std::forward<TCallable>(callable)](auto&&... args) -> std::invoke_result_t<TCallable,decltype(args)...> {
        std::cout << "This is some additional functionality" << std::endl;
        return callable(std::forward<decltype(args)>(args)...);
    };
}

int main(int argc, char *argv[])
{
    auto callable1 = []() {
        std::cout << "test1" << std::endl;
    };

    auto callable2 = [](int arg) {
        std::cout << "test2: " << arg << std::endl;
    };

    auto wrapped1 = wrap(callable1);
    auto wrapped2 = wrap(callable2);

    static_assert(std::is_invocable_v<decltype(callable1)>); // OK
    static_assert(std::is_invocable_v<decltype(wrapped1)>); // fails
    static_assert(std::is_invocable_v<decltype(callable2), int>); // OK
    static_assert(std::is_invocable_v<decltype(wrapped2), int>); // fails
}

As the comments on the static_asserts indicate, the wrapper callables are not invocable in the same way as the original callables. What needs to be changed in order to achieve the desired functionality?

The given example was compiled using Visual Studio 2017 (msvc 15.9.0).

like image 398
Cybran Avatar asked Nov 24 '18 18:11

Cybran


1 Answers

This is probably a bug in MSVC's implementation of std::invoke_result or std::is_invocable (I can reproduce the issue here even with Visual Studio 15.9.2). Your code works fine with clang (libc++) and gcc and I don't see any reason why it shouldn't. However, you don't really need std::invoke_result here anyways, you can just have your lambda deduce the return type:

template<class TCallable>
auto wrap(TCallable&& callable) {
    return [callable = std::forward<TCallable>(callable)](auto&&... args) -> decltype(auto) {
        std::cout << "This is some additional functionality" << std::endl;
        return callable(std::forward<decltype(args)>(args)...);
    };
}

wich then also seems to work fine with MSVC…

Edit: As pointed out by Piotr Skotnicki in the comments below, decltype(auto) will prohibit SFINAE. To solve this issue, you can use a trailing return type instead:

template<class TCallable>
auto wrap(TCallable&& callable) {
    return [callable = std::forward<TCallable>(callable)](auto&&... args) -> decltype(callable(std::forward<decltype(args)>(args)...)) {
        std::cout << "This is some additional functionality" << std::endl;
        return callable(std::forward<decltype(args)>(args)...);
    };
}

wich will be a bit more typing but should work fine with SFINAE and also seems to work fine with MSVC…

like image 135
Michael Kenzel Avatar answered Sep 26 '22 07:09

Michael Kenzel