5

Consider the following function:

template <class T>
constexpr /* something */ f(T&& x) {
    // do something
}

and let's say that I want to do sfinae based on the type of a forwarded argument passed to a function called myfunction. One way to achieve this is:

template <class T>
constexpr auto f(T&& x) -> decltype(myfunction(std::forward<T>(x))) {
    // do something
}

instead of doing this, is there a way to do it at the template level:

// This code won't compile
template <class T, class R = decltype(myfunction(std::forward<T>(x)))>
constexpr R f(T&& x) {
    // do something
}

except that I don't have access to x yet so this code won't compile. Is there a way to achieve this only based on T (possibly using std::declval)?

Note: this is not an X/Y problem, it's just an example to illustrate where this situation happen: I don't know how to do SFINAE with forwarding without accessing the variable because for me the behavior of std::forward is still a little mysterious.

Vincent
  • 57,703
  • 61
  • 205
  • 388

2 Answers2

7

Yes, std::declval is indeed the key:

template <class T, class R = decltype(myfunction(std::declval<T>()))>
constexpr R f(T&& x) {
    // do something
}

This will either SFINAE out or R will be the return type of whichever overload of myfunction is chosen.

Your question makes it appear to me that you need a refresher on how reference-collapsing works; I suggest reading up in that area (this seems like a good starting point).

ildjarn
  • 62,044
  • 9
  • 127
  • 211
4

std::forward() has nothing to do with this question. Nor does constexpr. So let's start out with the super basic, a function that just returns its argument by value:

template <class T>
auto f(T x) -> decltype(x);

Sure, we could just use T, but that's too easy. Now the question, how do we lift that into a template parameter while still maintaining SFINAE (granted there's obviously no possible substitution failure here, but bear with me):

template <class T, class R = decltype(x)>
R f(T x);

So that doesn't work because there's no x yet (or worse, there's some unrelated x somewhere else that name lookup finds). We can use the argument names in a trailing-return-type because they're in scope, but we cannot use them as part of expression-SFINAE in default template arguments because they're not yet in scope.

But because these are template parameters, we don't care about their values. We just care about their types. This isn't an evaluated expression. So I don't need x, I need something that has the same type as x. A first go might be:

template <class T, class R = decltype(T{})>
R f(T x);

This works... as long as T is default-constructible. But I'm writing a template, I don't want to be making assumptions on types that I don't need to be making. So instead, I can do something like:

template <class T> T make(); // never defined

template <class T, class R = decltype(make<T>())>
R f(T x);

And now we have our arbitrary expression of type T, that we can use in decltype, in a default template argument. Although, we're still a little limited with make() - there are types that you can't return by value from a functon (e.g. arrays), so it turns out to be more useful to add references. And we need a less-likely-to-collide name than make:

template <class T>
add_rvalue_reference<T> declval();

template <class T, class R = decltype(declval<T>())>
R f(T x);

This is precisely the point of std::declval<T> - to give you an expression of type T in an unevaluated context.


Back to your original problem. We use the same thought process of how to get from decltype(x) to decltype(declval<T>()), but just apply it to a different expression. Instead of x, we have myfunction(std::forward<T>(x)). Which is to say, we're invoking myfunction with the same type as our argument:

template <class T, class R = decltype(myfunction(std::declval<T&&>()))>
R f(T&& x);

But due to reference collapsing rules, std::declval<T&&> is actually the same function as std::declval<T>, so we can just write:

template <class T, class R = decltype(myfunction(std::declval<T>()))>
R f(T&& x);
Barry
  • 286,269
  • 29
  • 621
  • 977