Handling a void variable in a templatized function in C++11

孤街醉人 提交于 2020-01-24 04:29:22

问题


I have a template class that must perform some operation before calling a function whose parameters and return type are generic.

This is the method:

template <typename ReturnType, typename ...Args>
ReturnType function (Args ...args) {
  // prepare for call
  // ...
  ReturnType rv = makeCall(args...);  // [1]
  // dismiss the call
  // ...
  return rv;
}

Of course it's compiling correctly when ReturnType is not void. When I use it in this context:

function<void>(firstArg, secondArg);

The compiler responds with

error: return-statement with a value, in function returning 'void' [-fpermissive]

pointing to the line marked with [1].

Is there any solution other than passing -fpermissive to the compiler? I would prefer to have a unique method, because I possible solution I found is to instantiate different versions using enable_if and is_same.

Thank you in advance.

-- Update --

This is a complete example. I should have said that our functions are indeed class methods.

#include <type_traits>
#include <iostream>

class Caller {
public:
    Caller() {}

    template <typename ReturnType, typename ...Arguments>
    ReturnType call(Arguments ... args) {
        prepare();

        ReturnType rv = callImpl<ReturnType>(args...);

        done();

        return rv;
    }

private:
    void prepare() {
        std::cout << "Prepare\n";
    }

    void done() {
        std::cout << "Done\n";
    }

    template <typename ReturnType, typename ...Arguments>
    typename std::enable_if<std::is_same<ReturnType, void>::value, ReturnType>::type callImpl ( Arguments ... args) {
        std::cout << "Calling with void\n";
        return;
    }

    template <typename ReturnType, typename ...Arguments>
    typename std::enable_if<std::is_same<ReturnType, bool>::value, ReturnType>::type callImpl (Arguments ... args) {
        std::cout << "Calling with bool\n";
        return true;
    }

    template <typename ReturnType, typename ...Arguments>
    typename std::enable_if<std::is_same<ReturnType, int>::value, ReturnType>::type callImpl (Arguments ... args) {
        std::cout << "Calling with int\n";
        return 42;
    }
};


int main(int argc, char *argv[]) {

    Caller c;
    auto rbool = c.call<bool> (1,20);
    std::cout << "Return: " << rbool << "\n";
    auto rint = c.call<int> (1,20);
    std::cout << "Return: " << rint << "\n";

    // the next line fails compilation. compile with --std=c++11
    c.call<void>("abababa");

    return 0;
}

-- Update --

Not a big issue: Use std::bind(&Caller::callImpl<ReturnType>, this, args).


回答1:


Here's my attempt at a general C++11-compliant solution that you can easily reuse.

Let's start by creating a simple type trait that converts void to an empty struct. This doesn't introduce any code repetition.

struct nothing { };

template <typename T>
struct void_to_nothing 
{
    using type = T;
};

template <>
struct void_to_nothing<void>
{
    using type = nothing;
};

template <typename T>
using void_to_nothing_t = typename void_to_nothing<T>::type; 

We also need a way to call an arbitrary function converting an eventual void return type to nothing:

template <typename TReturn>
struct helper
{
    template <typename TF, typename... Ts>
    TReturn operator()(TF&& f, Ts&&... xs) const
    {
        return std::forward<TF>(f)(std::forward<Ts>(xs)...);
    }
};

template <>
struct helper<void>
{
    template <typename TF, typename... Ts>
    nothing operator()(TF&& f, Ts&&... xs) const
    {
        std::forward<TF>(f)(std::forward<Ts>(xs)...);
        return nothing{};
    }
};

template <typename TF, typename... Ts>
auto with_void_to_nothing(TF&& f, Ts&&... xs)
    -> void_to_nothing_t<
           decltype(std::forward<TF>(f)(std::forward<Ts>(xs)...))>
{
    using return_type = 
        decltype(std::forward<TF>(f)(std::forward<Ts>(xs)...));

    return helper<return_type>{}(std::forward<TF>(f), std::forward<Ts>(xs)...);
}

Usage:

template <typename ReturnType, typename ...Args>
void_to_nothing_t<ReturnType> function (Args ...args) {
  // prepare for call
  // ...
  auto rv = with_void_to_nothing(makeCall, args...);  // [1]
  // dismiss the call
  // ...
  return rv;
}

live wandbox example


There's a proposal by Matt Calabrese called "Regular Void" that would solve this issue. You can find it here: "P0146R1".




回答2:


Depending on what you wish to accomplish in the lines

// dismiss the call

you might be able to use:

template <typename ReturnType, typename ...Args>
ReturnType function (Args ...args) {
  // prepare for call
  // ...

  CallDismisser c;
  return  makeCall(args...);  // [1]
}

That would work as long as the destructor of CallDismisser can do everything you need to do.




回答3:


struct nothing {};


template<class Sig>
using returns_void = std::is_same< std::result_of_t<Sig>, void >;

template<class Sig>
using enable_void_wrap = std::enable_if_t< returns_void<Sig>{}, nothing >;
template<class Sig>
using disable_void_wrap = std::enable_if_t< !returns_void<Sig>{}, std::result_of_t<Sig> >;

template<class F>
auto wrapped_invoker( F&& f ) {
  return overload(
    [&](auto&&...args)->enable_void_wrap<F(decltype(args)...)> {
      std::forward<F>(f)(decltype(args)(args)...);
      return {};
    },
    [&](auto&&...args)->disable_void_wrap<F(decltype(args)...)> {
      return std::forward<F>(f)(decltype(args)(args)...);
    }
  );
}

so wrapped_invoker takes a function object, and makes it return nothing instead of void.

Next, holder:

template<class T>
struct holder {
  T t;
  T&& get()&& { return std::forward<T>(t); }
};
template<>
struct holder<void> {
  template<class T>
  holder(T&&) {} // discard
  void get()&& {}
};

holder lets you hold the return value and convert back to void if needed. You must create holder<T> using {} to get reference lifetime extension to work properly. Adding a ctor to holder<T> will break it.

holder<void> silently discards anything passed to it.

template <typename ReturnType, typename ...Args>
ReturnType function (Args ...args) {
  // prepare for call
  // ...
  holder<ReturnType> rv{ wrapped_invoker(makeCall)(args...) };
  // dismiss the call
  // ...
  return std::move(rv).get();
}

Now, holder<ReturnType> holds either nothing or the return value of makeCall(args...).

If it holds nothing, rv.get() returns void, and it is legal to return void to a function where ReturnValue is void.

Basically we are doing two tricks. First, we are preventing makeCall from returning void, and second if we are returning void we are discarding the return value of makeCall conditionally.

overload isn't written here, but it is a function that takes 1 or more function objects (such as lambdas) and returns their overload set. There is a proposal for std::overload, and a myriad of examples on stackoverflow itself.

Here is some:

  • Overloaded lambdas in C++ and differences between clang and gcc
  • C++11 “overloaded lambda” with variadic template and variable capture



回答4:


The problem seems to be with //Dismiss the call.

This code shouldn't exist. That's what we have RAII for. The following code does work, even with ReturnType = void.

template <typename ReturnType, typename ...Arguments>
ReturnType call(Arguments ... args) {
    Context cx;
    return callImpl<ReturnType>(args...);
}

Context::Context()  { std::cout << "prepare\n"; }
Context::~Context() { std::cout << "done\n"; }


来源:https://stackoverflow.com/questions/42725427/handling-a-void-variable-in-a-templatized-function-in-c11

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!