Function argument returning void or non-void type

后端 未结 4 1042
心在旅途
心在旅途 2021-01-17 10:37

I am in the middle of writing some generic code for a future library. I came across the following problem inside a template function. Consider the code below:



        
相关标签:
4条回答
  • 2021-01-17 11:17

    Some specialization, somewhere, is necessary. But the goal here is to avoid specializing the function itself. However, you can specialize a helper class.

    Tested with gcc 9.1 with -std=c++17.

    #include <type_traits>
    #include <iostream>
    
    template<typename T>
    struct return_value {
    
    
        T val;
    
        template<typename F, typename ...Args>
        return_value(F &&f, Args && ...args)
            : val{f(std::forward<Args>(args)...)}
        {
        }
    
        T value() const
        {
            return val;
        }
    };
    
    template<>
    struct return_value<void> {
    
        template<typename F, typename ...Args>
        return_value(F &&f, Args && ...args)
        {
            f(std::forward<Args>(args)...);
        }
    
        void value() const
        {
        }
    };
    
    template<class F>
    auto foo(F &&f)
    {
        return_value<decltype(std::declval<F &&>()(2, 4))> r{f, 2, 4};
    
        // Something
    
        return r.value();
    }
    
    int main()
    {
        foo( [](int a, int b) { return; });
    
        std::cout << foo( [](int a, int b) { return a+b; }) << std::endl;
    }
    
    0 讨论(0)
  • In case you need to use result (in non-void cases) in "some generic stuff", I propose a if constexpr based solution (so, unfortunately, not before C++17).

    Not really elegant, to be honest.

    First of all, detect the "true return type" of f (given the arguments)

    using TR_t = std::invoke_result_t<F, As...>;
    

    Next a constexpr variable to see if the returned type is void (just to simplify a little the following code)

    constexpr bool isVoidTR { std::is_same_v<TR_t, void> };
    

    Now we define a (potentially) "fake return type": int when the true return type is void, the TR_t otherwise

    using FR_t = std::conditional_t<isVoidTR, int, TR_t>;
    

    Then we define the a smart pointer to the result value as pointer to the "fake return type" (so int in void case)

    std::unique_ptr<FR_t>  pResult;
    

    Passing through a pointer, instead of a simple variable of type "fake return type", we can operate also when TR_t isn't default constructible or not assignable (limits, pointed by Barry (thanks), of the first version of this answer).

    Now, using if constexpr, the two case to exec f (this, IMHO, is the ugliest part because we have to write two times the same f invocation)

    if constexpr ( isVoidTR )
       std::forward<F>(f)(std::forward<As>(args)...);
    else
       pResult.reset(new TR_t{std::forward<F>(f)(std::forward<As>(args)...)});
    

    After this, the "some generic stuff" that can use result (in non-void cases) and also `isVoidTR).

    To conclude, another if constexpr

    if constexpr ( isVoidTR )
       return;
    else
       return *pResult;
    

    As pointed by Barry, this solution has some important downsides because (not void cases)

    • require an allocation
    • require an extra copy in correspondence of the return
    • doesn't works at all if the TR_t (the type returned by f()) is a reference type

    Anyway, the following is a full compiling C++17 example

    #include <memory>
    #include <type_traits>
    
    template <typename F, typename ... As>
    auto foo (F && f, As && ... args)
     {
       // true return type
       using TR_t = std::invoke_result_t<F, As...>;
    
       constexpr bool isVoidTR { std::is_same_v<TR_t, void> };
    
       // (possibly) fake return type
       using FR_t = std::conditional_t<isVoidTR, int, TR_t>;
    
       std::unique_ptr<FR_t>  pResult;
    
       if constexpr ( isVoidTR )
          std::forward<F>(f)(std::forward<As>(args)...);
       else
          pResult.reset(new TR_t{std::forward<F>(f)(std::forward<As>(args)...)});
    
       // some generic stuff (potentially depending from result,
       // in non-void cases)
    
       if constexpr ( isVoidTR )
          return;
       else
          return *pResult;
     }
    
    int main ()
     {
       foo([](){});
    
       //auto a { foo([](){}) };  // compilation error: foo() is void
    
       auto b { foo([](auto a0, auto...){ return a0; }, 1, 2l, 3ll) };
    
       static_assert( std::is_same_v<decltype(b), int> );
    
     }
    
    0 讨论(0)
  • 2021-01-17 11:29

    The best way to do this, in my opinion, is to actually change the way you call your possibly-void-returning functions. Basically, we change the ones that return void to instead return some class type Void that is, for all intents and purposes, the same thing and no users really are going to care.

    struct Void { };
    

    All we need to do is to wrap the invocation. The following uses C++17 names (std::invoke and std::invoke_result_t) but they're all implementable in C++14 without too much fuss:

    // normal case: R isn't void
    template <typename F, typename... Args, 
        typename R = std::invoke_result_t<F, Args...>,
        std::enable_if_t<!std::is_void<R>::value, int> = 0>
    R invoke_void(F&& f, Args&&... args) {
        return std::invoke(std::forward<F>(f), std::forward<Args>(args)...);
    }
    
    // special case: R is void
    template <typename F, typename... Args, 
        typename R = std::invoke_result_t<F, Args...>,
        std::enable_if_t<std::is_void<R>::value, int> = 0>
    Void invoke_void(F&& f, Args&&... args) {
        // just call it, since it doesn't return anything
        std::invoke(std::forward<F>(f), std::forward<Args>(args)...);
    
        // and return Void
        return Void{};
    }
    

    The advantage of doing it this way is that you can just directly write the code you wanted to write to begin with, in the way you wanted to write it:

    template<class F>
    auto foo(F &&f) {
        auto result = invoke_void(std::forward<F>(f), /*some args*/);
        //do some generic stuff
        return result;
    }
    

    And you don't have to either shove all your logic in a destructor or duplicate all of your logic by doing specialization. At the cost of foo([]{}) returning Void instead of void, which isn't much of a cost.

    And then if Regular Void is ever adopted, all you have to do is swap out invoke_void for std::invoke.

    0 讨论(0)
  • 2021-01-17 11:30

    if you can place the "some generic stuff" in the destructor of a bar class (inside a security try/catch block, if you're not sure that doesn't throw exceptions, as pointed by Drax), you can simply write

    template <typename F>
    auto foo (F &&f)
     {
       bar b;
    
       return std::forward<F>(f)(/*some args*/);
     }
    

    So the compiler compute f(/*some args*/), exec the destructor of b and return the computed value (or nothing).

    Observe that return func();, where func() is a function returning void, is perfectly legal.

    0 讨论(0)
提交回复
热议问题