1

I guess any use of SFINAE could be considered a hack, but here I tried for a long time and the best I could manage was to use a default void* argument in one of the overloads:

struct Dog 
{
    Dog() {}
    void makeNull() {}
    
};

// If no .makeNull() function this is eliminated
template <typename T>
constexpr auto HasMakeNullFunction() -> decltype(std::declval<T>().makeNull(), bool())
{
    return true;
}
// And this one is called. But I could only manage to do it with a default void* p = nullptr
template <typename T>
constexpr bool HasMakeNullFunction(void* p = nullptr)
{
    return false;
}

int main()
{

    constexpr bool b = HasMakeNullFunction<Dog>(); // True
    constexpr bool b2 = HasMakeNullFunction<int>(); // False

}

What's the way you're supposed to do it? This does work, but the typical way to use SFINAE is with a specialized version that gets called when the substitution fails, right? Also, I don't like the use of the default void* as I could see a potential for a misuse and an implicit conversion to void*.

Zebrafish
  • 11,682
  • 3
  • 43
  • 119
  • What version of C++ would you prefer the answer to target? – Patrick Roberts May 05 '21 at 02:28
  • @PatrickRoberts C++17. Or if C++20 has any better solutions, I'd like to see that too. – Zebrafish May 05 '21 at 02:36
  • I'm hesitant to say this is a duplicate of https://stackoverflow.com/q/257288/1896169 , but that question should have enough information to implement `HasMakeNullFunction` – Justin May 05 '21 at 02:42
  • *This does work* It really shouldn't. When calling it with `Dog` nothings stops the nullptr version from being valid, so the call is [ambiguous](https://godbolt.org/z/v479hnb3P). – super May 05 '21 at 03:48
  • @super My reasoning for why it works was that it prefers the non default argument version. No arguments are given so the first preference should be the function with no arguments. – Zebrafish May 05 '21 at 04:02
  • Stating that it works when the code doesn't compile seems unnecessarily unclear, if not outright wrong. – super May 05 '21 at 07:03
  • I just saw from the other comments on the answer that it does actually compile with MSVC. That's clearly non-conformant, but also explains your reasoning. – super May 05 '21 at 07:06

2 Answers2

2

Your code doesn't work since when specifying Dog as template argument the calling to HasMakeNullFunction is ambiguous.

You can define a type trait to separate the two overloads completely. e.g.

template <typename T, typename = void>
struct has_makeNull : std::false_type {};
template <typename T>
struct has_makeNull<T, decltype(std::declval<T>().makeNull(), void())> : std::true_type {};

template <typename T>
constexpr auto HasMakeNullFunction() -> std::enable_if_t<has_makeNull<T>::value, bool>
{
    return true;
}
template <typename T>
constexpr auto HasMakeNullFunction() -> std::enable_if_t<!has_makeNull<T>::value, bool>
{
    return false;
}

LIVE

songyuanyao
  • 169,198
  • 16
  • 310
  • 405
  • In this case, the `HasMakeNullFunction` is redundant with `has_makeNull` – Justin May 05 '21 at 02:42
  • @Justin You mean to overload `HasMakeNullFunction` without `has_makeNull`? But how to put constraint that doesn't have `makeNull` on the 2nd overload `HasMakeNullFunction`? – songyuanyao May 05 '21 at 02:52
  • Sorry, I meant that rather than `HasMakeNullFunction()`, the user could just do `has_makeNull::value` – Justin May 05 '21 at 03:01
  • @Justin I see. I suppose OP wants to constrol overload resolution by SFINAE, `HasMakeNullFunction` could be sample for it, with the help of `has_makeNull`. – songyuanyao May 05 '21 at 03:04
  • Strange, my code in the question compiles on my Visual Studio compiler. – Zebrafish May 05 '21 at 03:57
  • 1
    @Zebrafish I guess it's VS's issue; especially both [clang](https://wandbox.org/permlink/A0JFjFNg1KXiA25t) and [gcc](https://wandbox.org/permlink/jrWfMcd0FSQllGzD) say no. :) – songyuanyao May 05 '21 at 04:01
2

Before C++20 concepts, it was typical to define a struct that conditionally derived from either std::true_type or std::false_type in <type_traits>:

#include <type_traits>

template <class, class = void>
struct HasMakeNullFunction : std::false_type {};

template <class T>
struct HasMakeNullFunction<T, std::void_t<decltype(std::declval<T>().makeNull())>>
    : std::true_type {};

struct Dog 
{
    Dog() {}
    void makeNull() {}
};

int main()
{
    constexpr bool b = HasMakeNullFunction<Dog>::value; // true
    constexpr bool b2 = HasMakeNullFunction<int>::value; // false

    static_assert(b);
    static_assert(!b2);
}

godbolt.org

However, with concepts, it's even easier:

template <class T>
concept HasMakeNullFunction = requires (T v) {
    { v.makeNull() };
};

struct Dog 
{
    Dog() {}
    void makeNull() {}
};

int main()
{
    constexpr bool b = HasMakeNullFunction<Dog>; // true
    constexpr bool b2 = HasMakeNullFunction<int>; // false

    static_assert(b);
    static_assert(!b2);
}

godbolt.org

Patrick Roberts
  • 49,224
  • 10
  • 102
  • 153
  • `struct HasMakeNullFunction().makeNull())>` should probably be something like `std::void_t` – Justin May 05 '21 at 02:47
  • @Justin it really doesn't matter. That's just convention, but it's not necessary to make it work. – Patrick Roberts May 05 '21 at 02:48
  • 1
    Without `std::void_t`, you're requiring that `makeNull()` returns `void`. With `std::void_t`, you're allowing other return types. – Justin May 05 '21 at 02:50
  • @Justin good catch, I've edited my answer. – Patrick Roberts May 05 '21 at 02:55
  • 1
    You don't need `std::void_t`, you can say: `decltype(whatever(), void())` – Vittorio Romeo May 05 '21 at 03:05
  • @VittorioRomeo What is the type of decltype(void())? void() looks like a function signature. The type of decltype(int()) is int because the expression int() returns a value-initialized int. But what does 'void ()' mean? – Zebrafish May 05 '21 at 04:18
  • @Zebrafish `void()` is a value-initialized `void` :) It may be an [incomplete type](https://en.cppreference.com/w/cpp/language/type#Incomplete_type), but even incomplete types are allowed in [unevaluated expressions](https://en.cppreference.com/w/cpp/language/expressions#Unevaluated_expressions) such as within `decltype()` – Patrick Roberts May 05 '21 at 04:52
  • @PatrickRoberts I wonder why void() doesn't parse as a function signature, such as how std::function takes it. – Zebrafish May 05 '21 at 05:17
  • @Zebrafish probably because the parser is expecting a value expression at that position rather than a type expression, given the comma operator. – Patrick Roberts May 05 '21 at 05:22
  • @PatrickRoberts Because of the comma operator or because it's inside a decltype( )? – Zebrafish May 05 '21 at 05:25
  • @Zebrafish: Why do you expect signature for `decltype(void())` but `int` for `decltype(int())` (both have same syntax; I might understand that you expect error for `void()` as incomplete type). – Jarod42 May 05 '21 at 08:43
  • @Jarod42 I've settled on the understanding that int() is creating a temporary that's value-initialized (zero-initiailized) because int is a primitive/built-in type/no user-defined constructor/however-you-want-to-look-at-it. That's hard to picture with a void type. For example 'int func() { return int(); } Create an int temporary/rvalue and return it. versus 'void func() { return void();} Create a void temporary/rvalue and return it? – Zebrafish May 05 '21 at 14:06
  • @Zebrafish your example is syntactically valid but ill-formed because `void` is an incomplete type. But that doesn't matter when it's in an unevaluated context like `decltype`. – Patrick Roberts May 06 '21 at 02:06