"anti-SFINAE"在给定表达式格式不正确时启用重载



如果某个表达式格式不佳,使用SFINAE很容易隐藏特定的函数重载。但是我想做相反的事情,当且仅当给定表达式格式良好时隐藏重载,并且以一种非常通用的方式这样做。我有一个在clang 3.5.0和gcc 5.2.0中工作的解决方案,但我对任何评论和替代方案感兴趣。

理想情况下,应该有一个内置的constexpr bool函数/宏,在编译时告诉我们一个特定的表达式是否格式良好。

IS_WELL_FORMED(  declval<T>() < declval<T>()  )  // I want this as bool

可以与enable_if一起使用来启用或禁用过载。

我已经找到了一个解决方案,但是我在g++ 5.2.0和clang 3.5.0中遇到了一些奇怪的行为,我想知道是否有bug。


建议的解决方案

首先,是目前为止我发现的最健壮的解决方案,它在两个编译器上都能工作。例如,我想测试T是否有.length()方法。这需要将表达式"隐藏"在另一个模板中。还有一个名为well_formed_default的函数,我将在后面讨论。

    // Define a template to contain our expression
    template<typename T2=T, typename = 
        decltype( declval<T2>().length() ) // This line is the expression to test
    > struct test_for_length_method { };

,下面是它在包含类中的用法:

template<typename T>
struct Demo { // the main struct I'm working on
    // Define a template to "hide" our expression
    template<typename T2=T, typename = 
        decltype( declval<T2>().length() ) // This line is the expression to test
    > struct test_for_length_method { };
    // test if the above "test" succeeds
    constexpr bool T_has_length =
        well_formed_default< test_for_length_method >();
    // demonstrate the bool in an enable_if
    template<bool b = T_has_length>
    static
    auto demo_enable_if() -> typename std::enable_if<  b >::type { 
        cout << "T has length" << endl;
    }
    template<bool b = T_has_length>
    static
    auto demo_enable_if() -> typename std::enable_if< !b >::type {
        cout << "T doesn't" << endl;
    }
}

上述工作与Demo<int>::demo_enable_if()Demo<std::string>::demo_enable_if()的预期。

我不能在enable_if中直接使用T_has_length,因为它不是模板替换,因此会导致硬错误。因此,我通过在另一个模板参数bool b = T_has_length中复制它来假装它是一个模板参数。这类似于我们必须在test结构体中使用typename T2=T的方式。有点烦人,但我想这是有道理的。

我现在定义well_formed_default。它接受一个模板(以及可选的某些类型),并根据是否可以用这些特定参数构造模板返回true或false。我会将它自动包含在我所有的项目中。也许这样的东西已经存在于标准中?

template<template<class...> class Template, class ...Args>
constexpr auto well_formed_default_impl(int) 
    -> typename std::conditional<false,Template<Args...>, bool>::type {
        return true;
}
template<template<class...> class Template, class ...Args>
constexpr auto well_formed_default_impl(...)
    -> bool {
        return false; 
}   
template<template<class...> class Template, class ...Args>
constexpr bool well_formed_default() {
    return well_formed_default_impl<Template,Args...>(0);
}

,

我的(第一个)问题

在g++ 5.2.0和clang 3.5.0中都可以使用。但应该这样吗?这是完全的标准,还是我把标准推得太远了?我想对我来说最奇怪的事情是在well_formed_default_impl中使用Template<Args...> -这保证是我使用它的方式的替换失败吗?例如,当相关的decltype不是格式良好时,使用test_for_pushback_method_struct<> ?

,

为什么我不能使用别名代替?

(对于这个问题的其余部分,看看Coliru上的这段代码的输出可能会有所帮助,因为它包含了我在下面讨论的所有测试和结果。)

我用一个别名而不是上面的结构体开始这个项目。我想应该是相等的。但是,在两个编译器中,它们都认为string没有有length方法。

template<typename T2=T>
    using test_for_length_method_alias = decltype( declval<T2>().length() );

最后,我尝试了结构体和别名,但是我显式地定义了第一个类型参数(T),而不是依赖于默认的T2=T。这应该不会改变任何东西,因为我传递的是默认类型——但它确实改变了行为!使用带有显式第一个形参的结构体

well_formed_default<test_for_length_method_struct , T>

在两个编译器上都能正常工作。但是带有explicit-first-type的别名只能在clang:

下正确工作。
well_formed_default<test_for_length_method_alias , T>

can_apply模板:

template<class...>struct voider{using type=void;};
template<class...Ts>using void_t=typename voider<Ts...>::type;
namespace details {
  template<template<class...>class Z, class, class...>
  struct can_apply:std::false_type{};
  template<template<class...>class Z, class...Ts>
  struct can_apply<Z, void_t<Z<Ts...>>, Ts...>:
    std::true_type{};
}
template<template<class...>class Z, class...Ts>
using can_apply=details::can_apply<Z, void, Ts...>;

对于特定的问题,如果x<y使用它看起来像:

template<class Lhs, class Rhs>
using less_r = decltype( std::declval<Lhs>() < std::declval<Rhs>() );
template<class Lhs, class Rhs>
using can_less = can_apply< less_r, Lhs, Rhs >;
然后使用can_less:
struct Foo {};
struct Bar {};
void operator<( Foo, Bar ) {}
int main() {
  std::cout << can_less<Foo, Bar>{} << can_less<Bar,Foo>{} << can_less<int, int>{} << can_less<char*, int>{} << 'n';
}

输出在clang和gcc中都是1010

这在标准下工作,它大致匹配std::experimental::is_detected的接口(功能更少,但更容易实现)。

不太可能对语言进行任何更改以直接支持这种用法(除了将带来的概念)。概念将使编写互斥的重载更容易,而否定的概念允许编写重载如果表达式格式不佳,则启用。(是的,我知道这并不能帮助您为c++ 11或c++ 14编写这样的代码。)例子:

#include <iostream>
template <class T>
requires !requires(T t) {t.f();}
void f()
{
    std::cout << "does not support f()!" << std::endl;    
}
template <class T>
void f()
{
    std::cout << "supports f()!" << std::endl;    
}
struct A {};
struct B {void f() {}};
int main()
{
    f<A>();
    f<B>();
}

我添加无约束的重载只是为了说明目的。省略它会使调用f()格式错误。这些代码现在可以在gcc主干上运行,您可以在melpon.org/wandbox上试用。

最新更新