5

I'm new to writing template metaprogramming code (vs. just reading it). So I'm running afoul of some noob issues. One of which is pretty well summarized by this non-SO post called "What happened to my SFINAE?", which I will C++11-ize as this:

(Note: I gave the methods different names only to help with my error diagnosis in this "thought experiment" example. See @R.MartinhoFernandes's notes on why you wouldn't actually choose this approach in practice for non-overloads.)

#include <type_traits>

using namespace std;

template <typename T>
struct Foo {
    typename enable_if<is_pointer<T>::value, void>::type
    valid_if_pointer(T) const { }

    typename disable_if<is_pointer<T>::value, void>::type
    valid_if_not_pointer(T) const { }
};

int main(int argc, char * argv[])
{
    int someInt = 1020;
    Foo<int*>().valid_if_pointer(&someInt);    
    Foo<int>().valid_if_not_pointer(304);

    return 0;
}

@Alf says what happened to the SFINAE is "It wasn't there in the first place", and gives a suggestion that compiles, but templates the functions instead of the class. That might be right for some situations, but not all. (For instance: I'm specifically trying to write a container that can hold types that may or may not be copy-constructible, and I need to flip methods on and off based on that.)

As a workaround, I gave this a shot...which appears to work correctly.

#include <type_traits>

using namespace std;

template <typename T>
struct FooPointerBase {
    void valid_if_pointer(T) const { }
};

template <typename T>
struct FooNonPointerBase {
    void valid_if_not_pointer(T) const { }
};

template <typename T>
struct Foo : public conditional<
    is_pointer<T>::value, 
    FooPointerBase<T>,
    FooNonPointerBase<T> >::type {
};

int main(int argc, char * argv[])
{
    int someInt = 1020;
#if DEMONSTRATE_ERROR_CASES
    Foo<int*>().valid_if_not_pointer(&someInt);
    Foo<int>().valid_if_pointer(304);
#else
    Foo<int*>().valid_if_pointer(&someInt);
    Foo<int>().valid_if_not_pointer(304);
#endif
    return 0;
}

But if this is not broken (is it?), it's certainly not following a good general methodology for how to turn on and off methods in a templated class based on sniffing the type for traits. Is there a better solution?

Community
  • 1
  • 1
  • "That might be right for some situations, but not all." To be honest, the only situation I can think of is when you want a stable binary interface. Anyway, no, I don't think there's a better solution. – R. Martinho Fernandes Jul 17 '12 at 23:04
  • @R.MartinhoFernandes I'm confused...do you think there's little applicability for a container that sniffs type traits of its contained type, and has different methods turned on and off based on the sniff? *(I would cite boost::optional applied to a movable type, as I'm looking at that and some related issues.)* And are you saying you don't think there's a better solution than what I came up with, or you don't think there's a better solution than Alf's of de-templating the type? :-/ – HostileFork says dont trust SE Jul 17 '12 at 23:14
  • 3
    @HostileFork I don't think there's a solution better than templating the functions or yours. Both are good, I prefer templating the functions (like Flexo did below) because, in general, it's less trouble (what? a pair of new classes *for each criterion*?). And yes, your use case is perfectly fine. I just think that *unless you provide two overloads*, there's no need for SFINAE. Actually, I think SFINAE is worse. Gimme a while, I think this fits better in an answer ;) – R. Martinho Fernandes Jul 17 '12 at 23:23

2 Answers2

14

Firstly, C++11 did not carry forward boost's disable_if. So if you're going to transition boost code, you'll need to use enable_if with a negated condition (or redefine your own disable_if construct).

Secondly, for SFINAE to reach in and apply to the method level, those methods must be templates themselves. Yet your tests have to be done against those templates' parameters...so code like enable_if<is_pointer<T> will not work. You can finesse this by making some template argument (let's say X) default to be equal to T, and then throw in a static assertion that the caller has not explicitly specialized it to something else.

This means that instead of writing:

template <typename T>
struct Foo {
    typename enable_if<is_pointer<T>::value, void>::type
    valid_if_pointer(T) const { /* ... */ }

    typename disable_if<is_pointer<T>::value, void>::type
    valid_if_not_pointer(T) const { /* ... */ }
};

...you would write:

template <typename T>
struct Foo {
    template <typename X=T>
    typename enable_if<is_pointer<X>::value, void>::type
    valid_if_pointer(T) const {
        static_assert(is_same<X,T>::value, "can't explicitly specialize");
        /* ... */
    }

    template <typename X=T>    
    typename enable_if<not is_pointer<X>::value, void>::type
    valid_if_not_pointer(T) const {
        static_assert(is_same<X,T>::value, "can't explicitly specialize");
        /* ... */
    }
};

Both are now templates and the enable_if uses the template parameter X, rather than T which is for the whole class. It's specifically about the substitution that happens whilst creating the candidate set for overload resolution--in your initial version there's no template substitution happening during the overload resolution.

Note that the static assert is there to preserve the intent of the original problem, and prevent someone being able to compile things like:

Foo<int>().valid_if_pointer<int*>(someInt);
Community
  • 1
  • 1
Flexo
  • 87,323
  • 22
  • 191
  • 272
  • I did try exactly that... *(well I made them templates and added dummy parameters, I didn't think of the X=T trick)* . But I got errors (gcc 4.7) along the lines of `no type named ‘type’ in ‘struct std::enable_if`, and using your code I get the same kind of thing. Does this actually compile for you? – HostileFork says dont trust SE Jul 17 '12 at 23:06
  • @Flexo I grok the nullptr issue as it's nullptr_t (doh!). But even taking that out of the example I'm getting `‘struct Foo’ has no member named ‘valid_if_not_pointer’`. I'm on *g++ (Debian 4.7.0-8) 4.7.0*, could it be my compiler isn't current enough? – HostileFork says dont trust SE Jul 17 '12 at 23:22
  • Ah, I didn't paste from ideone...I pasted from the code above which uses `disable_if<` instead of `enable_if<!`. Perhaps you can explain that choice in your answer? It does make it work! – HostileFork says dont trust SE Jul 17 '12 at 23:28
  • Geez, that turns out to be the problem in the first place. Things I tried which would have worked if I had been just using `enable_if` caused errors, which I mistook as SFINAE problems when the real source of the problem was "disable_if is not defined" when you're not using boost! That piece of knowledge plus the `template` are the answer...I'll let you patch that up, and accept this! Thanks! – HostileFork says dont trust SE Jul 17 '12 at 23:43
  • 3
    This is broken. `X` needs to be in an non-deduced context, otherwise it could be different from `T`. For example your code accepts `Foo x; x.valid_if_not_pointer(0);`. – Johannes Schaub - litb Jul 18 '12 at 22:03
  • @JohannesSchaub-litb I edited it; can you confirm this is what you meant? – ecatmur Jul 18 '12 at 22:28
  • @ecatmur thanks. that's also better than what I had in mind. I thought about `typename identity::type`, but that's way more ugly than simply saying `T` :) – Johannes Schaub - litb Jul 18 '12 at 22:30
  • 1
    @JohannesSchaub-litb But if one is concerned about the types not matching, isn't the real answer to put in a check for the types being the same? Like `enable_if< is_same::value and is_pointer::value, void>::type`? With the test, it doesn't matter if you use X or T. Without it, you could have cases where you just wind up testing a bogus type...`like Foo().valid_if_pointer(someInt);` – HostileFork says dont trust SE Jul 19 '12 at 17:59
  • @Flexo In general, sounds like a good idea. But in this specific scenario, `valid_if_somecondition()` methods are really referring to a test on the initial T. The "X" is only being introduced to offer a proxy which can be used in the enable_if testing. So you pretty much want to disable any ability to explicitly pass it a type which might (for some "condition") give a different answer... – HostileFork says dont trust SE Jul 19 '12 at 18:16
  • 2
    Actually, a `static_assert( is_same::value, "cannot explicitly specialize this method" );` in the method body is probably more appropriate. – HostileFork says dont trust SE Jul 19 '12 at 18:30
  • @Flexo Hope you don't mind but I've digested the comments a bit into your answer, and done a little bit of rewrite. Wikipedia habits die hard. If you want to fix the style/tone back to your own, or any other changes, of course feel free. – HostileFork says dont trust SE Jul 19 '12 at 19:37
  • 1
    @HostileFork - edit looks good and I definitely don't mind about it being edited - that's the whole point of the site! I wish more people would make more edits rather than hit and run style Q&A personally. – Flexo Jul 20 '12 at 06:54
6

The way I see it you don't want SFINAE here. SFINAE is useful to pick between different templated overloads. Basically, you use it to help the compiler pick between template <typename Pointer> void f(Pointer); and template <typename NotPointer> void f(NotPointer);.

That's not what you want here. Here, you have two functions with different names, not two overloads of the same. The compiler can already pick between template <typename Pointer> void f(Pointer); and template <typename NotPointer> void g(NotPointer);.

I'll give an example to explain why I think SFINAE is not only unnecessary, but undesirable here.

Foo<int> not_pointer;
Foo<int*> pointer;

not_pointer.valid_if_pointer(); // #1
not_pointer.valid_if_not_pointer(); // #2
pointer.valid_if_pointer(); // #3
pointer.valid_if_not_pointer(); // #4

Now, let's say you managed to get this working with SFINAE. Attempting to compile this piece of code will yield errors on lines #1 and #4. Those errors will be something along the lines of "member not found" or similar. It may even list the function as a discarded candidate in overload resolution.

Now, let's say you didn't do this with SFINAE, but with static_assert instead. Like this:

template <typename T>
struct Foo {
    void valid_if_pointer(T) const {
        static_assert(std::is_pointer<T>::value, "valid_if_pointer only works for pointers");
        // blah blah implementation
    }

    void valid_if_not_pointer(T) const {
        static_assert(!std::is_pointer<T>::value, "valid_if_not_pointer only works for non-pointers");
        // blah blah implementation
    }
};

With this you'll get errors on the same line. But you'll get extremely short and useful errors. Something people have been asking of compiler writers for years. And it's now at your doorstep :)

You get the same thing: errors on both cases, except you get a much better one without SFINAE.

Also note that, if you didn't use static_assert at all and the implementation of the functions was only valid if given pointers or non-pointers, respectively, you would still get errors on the appropriate lines, except maybe nastier ones.

TL;DR: unless you have two actual template functions with the same name, it's preferable to use static_assert instead of SFINAE.

R. Martinho Fernandes
  • 228,013
  • 71
  • 433
  • 510
  • I appreciate your critique of the application, I'm aware of that part. I changed the names for testing purposes so I could eliminate confusion while debugging the mechanics. It really turns out the only issue I had with the alternatives I tried is that there is no `disable_if` in C++11, and I was mistaking the errors I was getting with my attempts as SFINAE problems when they were actually "no such thing as disable_if" problems! But I'm glad to have learned the `template` trick, that seems to be the right solution. – HostileFork says dont trust SE Jul 17 '12 at 23:37