C++17: Wrapping callable using generic variadic lambda
Asked Answered
U

1

5

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

Unearthly answered 24/11, 2018 at 18:8 Comment(0)
S
7

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

Southeasterly answered 24/11, 2018 at 18:25 Comment(6)
-> decltype(auto) is not SFINAE-friendly iirc, you should probably use -> decltype(callable(decltype(args)(args)...))Sawtelle
@PiotrSkotnicki I think you are right. Good point; I added that remark to my answer, thanks!Southeasterly
Thank you very much! The second solution works flawlessly, also in conjunction with SFINAE.Unearthly
I was actually not aware that the original callables could be dangling. Are you sure about this? I believed the capture works as a capture-by-value with perfect forwarding.Unearthly
@Unearthly Sorry, I must've been confused for a second there. The lambda capture should capture via auto semantics, so it should indeed make a copy/move construct. I removed that part of my answer.Southeasterly
@Unearthly would be great if you could file a Visual Studio bug for this and maybe link the bugreport here…Southeasterly

© 2022 - 2024 — McMap. All rights reserved.