How can I get the behavior of a plain `auto` return type when using "expression SFINAE"?

120 Views Asked by At

Consider the following code.

struct Widget {
    int& get();
};

template<typename X>
auto func_1(X& x) {
  return x.get();
}

template<typename X>
auto func_2(X& x) -> decltype(x.get()) {
  return x.get();
}

When called with an l-value of type Widget, the function func_1 will be instantiated with return type int where function func_2 will have return type int&.

Also, there is a difference between func_1 and func_2 in that "expression SFINAE" is performed for func_2. So, for types X which don't have a .get() member, func_2 will not participate in overload resolution.

My question is: how can we get the return type behavior of func_1 while still performing expression SFINAE?

The following func_3 seems to work in the cases I tested, but I feel there should be a simpler alternative. Also, I'm not sure if func_3 has exactly the same return type as func_1 in all cases.

template<typename X>
auto func_3(X& x) -> std::remove_cvref_t<std::decay_t<decltype(x.get())>> {
  return x.get();
}
3

There are 3 best solutions below

0
HolyBlackCat On BEST ANSWER

Just std::decay_t<decltype(x.get())> is enough. Or std::remove_cvref_t<decltype(x.get())> (the latter doesn't perform array/function to pointer decay).

Or, in C++23 you can use decltype(auto(x.get())).


But if you're wrapping a function, usually the plain decltype is more correct (e.g. in your case, if get() returns a reference, auto forces a copy).

1
康桓瑋 On

My question is: how can we get the return type behavior of func_1 while still performing expression SFINAE?

The following func_3 seems to work in the cases I tested, but I feel there should be a simpler alternative.

With the C++23 auto(x) language feature, you can

struct Widget {
    int& get();
};

template<typename X>
auto func_3(X& x) -> decltype(auto(x.get())) {
  return x.get();
}
0
Artyer On

Just move the SFINAE check to another place.

The noexcept specifier is a good place because it conserves noexcept-ness as an added benefit:

template<typename X>
auto func_2(X& x) noexcept(noexcept(x.get())) -> auto {
  return x.get();
}

Or if you have C++20 you can use a constraint:

template<typename X>
auto func_2(X& x) requires requires { x.get(); } {
  return x.get();
}

Or really any alternative method that doesn't use the return type:

template<typename X, std::void_t<decltype(x.get())>* = nullptr>
auto func_2(X& x) {
  return x.get();
}