rustyx
rustyx

Reputation: 85531

How to convert std::result_of to decltype in a template argument

At CppCon 2015, S. Lavavej from Microsoft said to avoid using result_of. But I have a situation in which I can't seem to be able to find a proper alternative.

Consider the following code. Is there any way to change std::result_of<F()>::type to use decltype instead?

#include <iostream>
#include <future>
#include <type_traits>

template<typename T>
struct NopErrCB
{
  constexpr T operator() () const { return T(); }
};

template <typename Func, typename ErrCB>
struct SafeTaskWrapper {
  Func f;
  ErrCB errCB;

  template <typename... T>
  auto operator()(T&&... args) -> decltype(f(args...)) {
    try
    {
      return f(std::forward<T>(args)...);
    }
    catch (...)
    {
      return errCB();
    }
  }

};
//                                   vvv OVER HERE  vvv
template <typename F>
SafeTaskWrapper<F, NopErrCB<typename std::result_of<F()>::type>> make_safe_task(F&& f) {
  return { std::forward<F>(f) };
}

template <typename F, typename ErrCB>
SafeTaskWrapper<F, ErrCB> make_safe_task(F&& f, ErrCB&& cb) {
  return { std::forward<F>(f), std::forward<ErrCB>(cb) };
}

int main() {
  auto futA = std::async(std::launch::async, make_safe_task([] { throw "A"; return 1; }));
  auto futB = std::async(std::launch::async, make_safe_task([] { throw "B"; return 1; }, [] { return 2; }));
  auto futC = std::async(std::launch::async, make_safe_task([] { throw "C"; }));
  std::cout << futA.get() << std::endl;
  std::cout << futB.get() << std::endl;
  futC.get();
}

PS. Please don't mind the purpose of the SafeTaskWrapper. I know a future already handles C++ exceptions. This is just a demo, the actual code is for handling Windows SEH exceptions, but that doesn't matter for this question.

Upvotes: 4

Views: 920

Answers (4)

Jarod42
Jarod42

Reputation: 218268

You might use

decltype(std::declval<F>()())

instead of

typename std::result_of<F()>::type

which are similar, but have subtle differences

Upvotes: 4

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275896

Yes, your use of result_of here causes a bug. And yes, there is a way to write your code without result_of that clears the bug. And we can do this without constructing an equivalently buggy decltype replacement.

First, note your result_of code has a bug.

struct evil {
  template<class...Args>
  int operator()(Args&&)&&{return 0;}
  template<class...Args>
  std::string operator()(Args&&)const&{return "hello";}
};

Pass an evil{} to your code, and it will fail to compile. That is because you evaluated f in SafeTaskWrapper in an lvalue context, while you calculated result_of in an rvalue context.

This is exactly the kind of error that S.T.L. was talking about.

Now, std::result_of_t<F(Args...)> is basically1 equivalent to decltype( std::declval<F>()( std::declval<Args>()... ) ), or in this case decltype( std::declval<F>()()). Replacing result_of_t with decltype in this sense is utterly useless.

What I'd do here is I'd change the rest of your code.

struct anything_really {
  template<class T> operator T() const { return {}; }
};
struct return_anything {
  anything_really operator()() const { return {}; }
};

this replaces your errCB type. Note that it isn't a template type.

Next, we have to handle the "don't return into void" problem. This does it:

template<class R>
struct smart_invoke {
  template<class F>
  R operator()(F&& f)const {
    return std::forward<F>(f)();
  }
};
template<>
struct smart_invoke<void> {
  template<class F>
  void operator()(F&& f)const {
    std::forward<F>(f)();
  }
};

Which gives us our final solution:

template <typename Func, typename ErrCB=return_anything>
struct SafeTaskWrapper {
  Func f;
  ErrCB errCB;

  template <class... Ts>
  decltype(auto) operator()(Ts&&... args) {
    try {
      return f(std::forward<T>(args)...);
    } catch (...) {
      using R=decltype( f( std::forward<T>(args)... ) );
      return smart_invoke<R>{}(errCB);
    }
  }
};

Now our factory functions. I could keep two, but I'm lazy.

template <class ErrCB=return_anything, class F>
SafeTaskWrapper<F, ErrCB> make_safe_task(F&& f, ErrCB&& cb = {}) {
  return { std::forward<F>(f), std::forward<ErrCB>(cb) };
}

I also swapped the template argument order, so you can pass in a different default-constructible ErrCB and not bother passing in a value while I was at it.

And result_of_t has been eliminated.

Also your trailing return decltype had an error in it. It should be exactly the same as the (key) return statement, not similar.


1 There are exceptions if F is a member function pointer or member or the like, but your code won't work in those cases anyhow. In C++17, you'll want to call std::invoke in your operator() and in the decltype return type clause as well. The other changes remain correct, and because we avoid deducing the return type in two spots, they remain unchanged.

Upvotes: 4

Richard Hodges
Richard Hodges

Reputation: 69912

The direct translation would be:

template <typename F>
SafeTaskWrapper<F, NopErrCB<decltype(std::declval<F>()())>> make_safe_task(F&& f) {
  return { std::forward<F>(f) };
}

Which seems no clearer to me.

Upvotes: 1

ikh
ikh

Reputation: 10427

That's where std::result_of is used. I think that is enough.

If you want shorter and more readable code, consider using std::result_of_t, which is added since C++14.

//NopErrCB<typename std::result_of<F()>::type>
NopErrCB<std::result_of_t<F()>> // equal

Upvotes: 2

Related Questions