7

It is easy to use SFINAE to hide a particular function overload if a particular expression is not well-formed. But I want to do the opposite, hiding an overload if and only if a given expression is well-formed, and to do so in a very general way. I have a solution that's working in clang 3.5.0 and gcc 5.2.0, but I'm interested in any comments and alternatives.

Ideally, there would be a builtin constexpr bool function/macro that would tell us at compile time whether a particular expression is well formed.

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

which could be used with enable_if to enable or disable overloads.

I have found a solution, but I have come across some strange behaviour in g++ 5.2.0 and clang 3.5.0 and I wonder if either are buggy.


A proposed solution

First, the most robust solution I've found so far, which works on both compilers. For example, I want to test if T has a .length() method. This requires "hiding" the expression inside another template. Also, a function called well_formed_default that I'll discuss later.

    // 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 { };

and here is how it might be used in a containing class:

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;
    }
}

The above works as expected with Demo<int>::demo_enable_if() and Demo<std::string>::demo_enable_if().

I can't use T_has_length directly inside the enable_if as it will lead to hard errors because it's not template substitution. Therefore, I pretend it is a template parameter by making a copy of if in another template parameter bool b = T_has_length. This is similar to how we have to use typename T2=T inside the test struct. A bit annoying, but I guess it makes sense.

I now define well_formed_default. It takes a template (and optionally some types) and returns true or false depending on whether it can construct the template with those particular args. I would include it automatically in all my projects. Perhaps something like this already exists in the standard?

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);
}

--

My (first) question

This works in both g++ 5.2.0 and clang 3.5.0. But should it? Is this fully standard, or am I pushing the standard too far? I guess the weirdest thing for me is the use of Template<Args...> inside well_formed_default_impl - is this guaranteed to be a substitution failure in the way I use it? e.g. with test_for_pushback_method_struct<> when the relevant decltype isn't well-formed?

--

Why can't I use an alias instead?

(For the remainder of this question, it might help to look at the output of this code on Coliru as it has all the tests and results I'm discussing below.)

I began this project with an alias instead of the above struct. I thought it would be equivalent. But instead, with both compilers, they think that string does not have a length method.

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

Finally, I tried both the struct and the alias, but where I explicitly define the first type parameter (T) instead of relying on the the default T2=T. This shouldn't change anything, because I'm passing the type that is the default - but it does change the behaviour! Using the struct with an explicit first parameter

well_formed_default<test_for_length_method_struct , T>

works correctly on both compilers. But the alias-with-explicit-first-type only works correctly with clang:

well_formed_default<test_for_length_method_alias , T>
Aaron McDaid
  • 26,501
  • 9
  • 66
  • 88
  • 2
    Are you aware of http://en.cppreference.com/w/cpp/experimental/is_detected ? – dyp Aug 14 '15 at 16:29
  • 1
    It seems this has to do with how the deduction works: http://melpon.org/wandbox/permlink/Q1ObwKmGatRZ4jDJ Either the `template class` parameter is not properly matched with the alias template, or the substitution fails. – dyp Aug 14 '15 at 17:10
  • 2
    Maybe this is http://wg21.cmeerw.net/cwg/issue1430 ? – dyp Aug 14 '15 at 17:13
  • 1
    http://stackoverflow.com/a/30195655/1774667 solve your problem? Is basically one part of `is_detected` above (the part you need). – Yakk - Adam Nevraumont Aug 14 '15 at 17:42
  • Thanks for the feedback and links. I'll have to study this more when I have time this weekend. I guess I'll be trying to understand if I there are core language features that are coming in C++17 to make this easier, or if these are just library extensions. Because if these techniques can all be implemented in C++11, as I have done in this question, then why hasn't this been used for the last few years as the standard why to do concept-liking checking?! Although I guess there might be some further issues and details I'm missing. – Aaron McDaid Aug 14 '15 at 21:26
  • @dyp, I'm only now seeing the relevance of the pack expansion issue. It took a lot of experimenting before I was able to find more examples that helped me. Basically, when computing the return type, gcc rejects expansion into aliases. But it accepts it, and computes the correct type, in other contexts such as specifying a default type for a template type parameter. It all seems a bit inconsistent. Anyway, the standard requires that templates can be expanded into templates? As clang does? And gcc accepts them in some places, but there are still some gaps (e.g. return type) in gcc? – Aaron McDaid Aug 15 '15 at 18:55
  • (following on from my comment just a few minutes ago). After some more experiments I'm suddenly getting confused again. @dyp, now clang is rejecting expansion of packs into aliases. Is there a good resource on alias templates generally? I assumed I understood them as they seem quite simple, but I must admit I don't. They seem to have some bizarre limitations that template `structs` don't have. – Aaron McDaid Aug 15 '15 at 19:04
  • 1
    I don't really understand where CWG is heading with alias templates. They're partially transparent, but I don't quite get to what degree they are. For example, http://wg21.cmeerw.net/cwg/issue1286 Additionally, CWG 1430 mentions name mangling. Since there's an open issue (1430) and implementation variance, I'd suggest reducing the code to this issue, and ask on std-discussion, or the gcc devs, or on SO again. – dyp Aug 15 '15 at 19:17

2 Answers2

5

Here is can_apply boilerplate:

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...>;

For the particular problem if x<y using it looks like:

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 >;

And then using 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';
}

output is 1010 in both clang and gcc.

This works under the standard, and it matches roughly the interface of a std::experimental::is_detected (with fewer features, but simpler to implement).

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • At first glance, this seems very similar to one of the experiments that failed for me with g++ (aliases misbehaved sometimes). So I'll have to do some more testing- - gradually turn your code into mine, or vice versa, to see what breaks down. – Aaron McDaid Aug 14 '15 at 21:36
4

It's somewhat unlikely that there will be any changes to the language to support this sort of uses directly (except what concepts will bring). Concepts will make it easier to write mutually exclusive overloads and negated concepts allow writing overloads that are enabled if an expression is not well-formed. (And yes, I know that that doesn't help you write such code for C++11 or C++14.) Example:

#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>();
}

I added the unconstrained overload just for illustration purposes. Leaving it out would make the call f<B>() ill-formed. The code works today on gcc trunk, and you can try it out on melpon.org/wandbox.

  • I guess I should have mentioned Concepts in my question as they are relevant. Thanks for mentioning them, as I don't really know much about them apart from watching a great talk by Stroustrup about them! – Aaron McDaid Aug 14 '15 at 21:40