c++ - improvements - ¿Por qué está encendido sfinae si constexpr no está permitido?
c++17 libro (3)
El idioma de detección funciona de la siguiente manera
template<typename T, typename = void>
struct has_foo {static constexpr bool value = false;};
template<typename T>
struct has_foo<T, std::void_t<decltype(&T::foo)>> {static constexpr bool value = true;};
template<typename T>
constexpr bool has_foo_v = has_foo<T>::value;
Y luego podemos detectar la presencia de foo
en cualquier tipo T
if constexpr(has_foo_v<decltype(var)>)
var.foo();
Mi problema es que es mucho para escribir (lea: quiero romper mi teclado para escribir), y me pregunté si lo siguiente es posible
if constexpr(std::void_t<decltype(&decltype(var)::foo)>(), true)
var.foo();
No lo es.
¿Hay una razón detrás de esto?
Más específicamente, ¿qué compensaciones se tendrán que hacer si esto se permitiera?
Desde c ++ 17 siempre hay una solución constexpr lambda si realmente necesita hacer sfinae en línea:
#include <utility>
template <class Lambda, class... Ts>
constexpr auto test_sfinae(Lambda lambda, Ts&&...)
-> decltype(lambda(std::declval<Ts>()...), bool{}) { return true; }
constexpr bool test_sfinae(...) { return false; }
template <class T>
constexpr bool bar(T var) {
if constexpr(test_sfinae([](auto v) -> decltype(v.foo()){}, var))
return true;
return false;
}
struct A {
void foo() {}
};
struct B { };
int main() {
static_assert(bar(A{}));
static_assert(!bar(B{}));
}
Su uso de la función de puntero a miembro es una mala idea; si foo
está sobrecargado, falla falsamente (tienes un foo, pero no solo uno). ¿Quién realmente quiere "tienes exactamente un foo"? Casi nadie.
Aquí hay una versión más breve:
template<class T>
using dot_foo_r = decltype( std::declval<T>().foo() );
template<class T>
using can_foo = can_apply<dot_foo_r, T>;
dónde
namespace details {
template<template<class...>class, class, class...>
struct can_apply:std::false_type{};
template<template<class...>class Z, class...Ts>
struct can_apply<Z, std::void_t<Z<Ts...>>, Ts...>:std::true_type{};
}
template<template<class...>class Z, class...Ts>
using can_apply = details::can_apply<Z, void, Ts...>;
Ahora, escribir dot_foo_r
es un poco molesto.
Con constexpr
lambdas podemos hacerlo menos molesto y hacerlo en línea.
#define RETURNS(...) /
noexcept(noexcept(__VA_ARGS__)) /
-> decltype(__VA_ARGS__) /
{ return __VA_ARGS__; }
Necesita la macro RETURNS
, al menos hasta que la presentación de @ Barry a [](auto&&f)RETURNS(f())
sea equivalente a [](auto&&f)=>f()
.
Luego escribimos can_invoke_f
, que es una variante constexpr
de std::is_invokable
:
template<class F>
constexpr auto can_invoke( F&& f ) {
return [](auto&&...args)->std::is_invokable<F(decltype(args)...)>{
return {};
};
}
Esto nos da:
if constexpr(
can_invoke([](auto&&var) RETURNS(var.foo()))(var)
) {
var.foo();
}
o usando la sintaxis C ++ 20 propuesta por @ Barry:
if constexpr(can_invoke(var=>var.foo())(var)) {
var.foo();
}
y hemos terminado.
El truco es que la macro RETURNS
(o la característica =>
C ++ 20) nos permite hacer SFINAE en una expresión. La lambda se convierte en una forma fácil de llevar esa expresión como un valor.
Usted podría escribir
[](auto&&var) ->decltype(var.foo()) { return var.foo(); }
pero creo que RETURNS
vale la pena (y no me gustan las macros).
También puede reducir la cantidad de código usando std::is_detected
.
En tu ejemplo, el código se vería así:
template <class T>
using has_foo_t = decltype(std::declval<T>().foo());
if constexpr(std::is_detected_v<has_foo_t,decltype(var)>)
var.foo();