`enable_if` with `enum` template specialization problem
Asked Answered
C

1

6

I have problems with GCC compiling enable_ifs applied to the return value of the templated class method. With Clang, I am able to use an expression in enable_if on the enum template argument, while GCC refuses to compile this code.

Here is the problem description, initial code, and its subsequent modifications that try to satisfy me and compilers (unfortunately, not simultaneously).

I have a non-templated class Logic that contains a templated class method computeThings() which has an enum Strategy as one of its template parameters. The logic in computeThings() depends on the compile-time Strategy, so if constexpr is a reasonable way to make an implementation.

Variant 1

#include <iostream>
class Logic {
public:
    enum Strategy { strat_A, strat_B };
    // class A and class B are dummy in this example, provided to show that there are several template
    // parameters, and strategy selection effectively results in 
    // partial (not full) templated method specification
    template <class A, class B, Strategy strategy>
    int computeThings();
};

template <class A, class B, Logic::Strategy strategy>
int Logic::computeThings() {
    if constexpr(strategy==strat_A)
        return 0;
    else
        return 1;
}

int main() {
    Logic mylogic;
    std::cout<<mylogic.computeThings<int,int,Logic::strat_A>()<<std::endl; //outputs 0
    std::cout<<mylogic.computeThings<int,int,Logic::strat_B>()<<std::endl; //outputs 1
    return 0;
}

Variant 1 works fine and compiles both in clang and GCC. However, I want to get rid of if constexpr and split computeThings() into two specialized methods based on the chosen Strategy. Reason: the function is performance-critical and contains a lot of code.

So, I am coming up with Variant 2 that uses enable_if applied to the return value.

Variant 2

#include <iostream>
class Logic {
public:
    enum Strategy { strat_A, strat_B };
    
    template <class A, class B, Logic::Strategy strategy>
    typename std::enable_if_t<strategy==Logic::strat_A,int>
    computeThings();
    
    template <class A, class B, Logic::Strategy strategy>
    typename std::enable_if_t<strategy==Logic::strat_B,int>
    computeThings();
};

template <class A, class B, Logic::Strategy strategy>
typename std::enable_if_t<strategy==Logic::strat_A,int>
Logic::computeThings() {
    return 0;
}

template <class A, class B, Logic::Strategy strategy>
typename std::enable_if_t<strategy==Logic::strat_B,int>
Logic::computeThings() {
    return 1;
}

int main() {
    Logic mylogic;
    std::cout<<mylogic.computeThings<int,int,Logic::strat_A>()<<std::endl; //outputs 0
    std::cout<<mylogic.computeThings<int,int,Logic::strat_B>()<<std::endl; //outputs 1
    return 0;
}

I am perfectly comfortable with variant 2 (though would appreciate feedback as well). This code compiles fine using AppleClang (and probably Clang, in general) and produces the right results. However, it fails to compile with GCC with the following error (+ the same but for the other method):

error: prototype for 'std::enable_if_t<(strategy == Logic:: strat_A),int> Logic::computeThings()' does not match any in class 'Logic' Logic::computeThings()

candidates are: template<class A, class B, Logic::Strategy strategy> std::enable_if_t<(strategy == strat_B), int> Logic::computeThings() computeThings();

candidates are: template<class A, class B, Logic::Strategy strategy> std::enable_if_t<(strategy == strat_A), int> Logic::computeThings() computeThings();

So, apparently, using a simple strategy==Logic::strat_A conflicts with GCC. So, I came up with a solution to this that satisfies both clang and GCC, which wraps strategy==Logic::strat_A into a struct:

Variant 3

#include <iostream>
class Logic {
public:
    enum Strategy { strat_A, strat_B };
    
    template <Logic::Strategy strategy> struct isStratA {
        static const bool value = strategy==Logic::strat_A;
    };
    
    template <class A, class B, Logic::Strategy strategy>
    typename std::enable_if_t<Logic::isStratA<strategy>::value,int>
    computeThings();
    
    template <class A, class B, Logic::Strategy strategy>
    typename std::enable_if_t<!Logic::isStratA<strategy>::value,int>
    computeThings();
};

template <class A, class B, Logic::Strategy strategy>
typename std::enable_if_t<Logic::isStratA<strategy>::value,int>
Logic::computeThings() {
    return 0;
}

template <class A, class B, Logic::Strategy strategy>
typename std::enable_if_t<!Logic::isStratA<strategy>::value,int>
Logic::computeThings() {
    return 1;
}

int main() {
    Logic mylogic;
    std::cout<<mylogic.computeThings<int,int,Logic::strat_A>()<<std::endl; //outputs 0
    std::cout<<mylogic.computeThings<int,int,Logic::strat_B>()<<std::endl; //outputs 1
    return 0;
}

with Variant 3, both Clang and GCC are happy. However, I am not, as I have to create a lot of dummy wrappers for an unknown reason (here, I have just one, but technically, I should have both isStratA<> and isStratB<>).

Questions:

  • do I violate any C++ standard (or common sense) in my Variant 2?
  • do I have an easy way of making a Variant 2-type solution working without going to dummy wrappers as in Variant 3?

(if that matters, GCC 7.4.0 and Apple LLVM version 10.0.0: clang-1000.11.45.5)

Chapell answered 28/2, 2019 at 0:14 Comment(11)
@NikitaKniazev not entirely sure what does that lead to in the given context.Chapell
You are trying to use enable_if which uses SFINAE to remove an overload, but there is no deduction in the examples. The successes on the second and third examples must be a compiler bug and you should not rely on this.Taveras
To be honest, I don't understand why do you insist on using enable_if if you are willing to write your template types by hand. Why wouldn't you simply do two functions computeThingsA and computeThingsB? It would also solve problem of Reason: the function is performance-critical and contains a lot of code. - just call appropriate function in constexpr ifVitreous
@Vitreous this is a simplified example. In reality, there are several Strategy enums and I am willing to trade some complications in template specialization in exchange to simpler calls in the other parts of the code.Chapell
It is hard to suggest you something as the usage lacks context. The closes thing is tag dispatch, but probably it is better to make Logic parameterizable with a strategy. FYI: the third example will not compile with MSVC godbolt.org/z/N0NI70Taveras
I am not still convinced it is "simpler call". Anyway, you can do variant 3 with just one wrapper isStrat: godbolt.org/z/fXNTWP EDIT: It passes on gcc, but still fails on MSVC as well, as @NikitaKniazev pointedVitreous
You stated "performance-critical" as a reason for this change. Have you done a test to see if there has been a performance gain yet? (If the compiler was already doing something equivalent to this behind the scenes, this could be a lot of work for no benefit.)Tris
@JaMiTnot exactly. The function is performance-critical and that's why it is templated and I do not want to have an additional run-time if condition inside it. This refactoring (var2/var3) obviously would not give any advantage over var1.Chapell
But there is no additional run-time if condition in variant 1... There's only a compile-time if condition.Stratigraphy
@Stratigraphy sure. there is none. In Variant 1 the problem is a large amount of code inside one function which I try to avoid by separating it into several different functions by using template specializations.Chapell
@AntonMenshov I would try breaking the code into several (new) functions, but call those functions from the existing if-else framework in Variant 1's computeThings(). In theory, each instantiation of that template would simplify to a single function call, which the compiler could inline. Separate functions, no additional template magic, and the same overhead. (But not an answer as to why variant 2 sometimes works/doesn't work.)Tris
F
6

As @bogdan said in the comments, this is most likely a compiler bug. Actually I noticed that it works if you use trailing return types in the out-of-line definitions of your function templates:

template <class A, class B, Logic::Strategy strategy>
auto Logic::computeThings() ->
std::enable_if_t<strategy==Logic::strat_A,int> {
    return 0;
}

template <class A, class B, Logic::Strategy strategy>
auto Logic::computeThings() ->
std::enable_if_t<strategy==Logic::strat_B,int> {
    return 1;
}

I prefer putting the enable_if in the type of a non-type template parameter with a default argument:

template <class A, class B, Logic::Strategy strategy,
          std::enable_if_t<strategy==Logic::strat_A,int> = 0>
int Logic::computeThings() {
    return 0;
}

template <class A, class B, Logic::Strategy strategy,
          std::enable_if_t<strategy==Logic::strat_B,int> = 0>
int Logic::computeThings() {
    return 1;
}

But SFINAE is far too complex of a feature for something so simple. There are much easier ways to do what you are trying to do. Take this example using tag dispatch:

#include <iostream>
#include <type_traits>

class Logic {
public:
    enum Strategy { strat_A, strat_B };

    template <class A, class B>
    int computeThings(std::integral_constant<Strategy, strat_A>);

    template <class A, class B>
    int computeThings(std::integral_constant<Strategy, strat_B>);
};

template <class A, class B>
int Logic::computeThings(std::integral_constant<Strategy, strat_A>) {
    return 0;
}

template <class A, class B>
int Logic::computeThings(std::integral_constant<Strategy, strat_B>) {
    return 1;
}

int main() {
    Logic mylogic;
    std::cout<<mylogic.computeThings<int,int>(
            std::integral_constant<Logic::Strategy, Logic::strat_A>{}
        )<<std::endl; //outputs 0
    std::cout<<mylogic.computeThings<int,int>(
            std::integral_constant<Logic::Strategy, Logic::strat_B>{}
        )<<std::endl; //outputs 1
    return 0;
}

That can be simplified further by getting rid of the enum and directly defining some tag types instead:

class Logic {
public:
    class strat_A {};
    class strat_B {};

    template <class A, class B>
    int computeThings(strat_A);

    template <class A, class B>
    int computeThings(strat_B);
};

template <class A, class B>
int Logic::computeThings(strat_A) { return 0; }

template <class A, class B>
int Logic::computeThings(strat_B) { return 1; }

int main() {
    Logic mylogic;
    std::cout<<mylogic.computeThings<int,int>(Logic::strat_A{})<<std::endl; //outputs 0
    std::cout<<mylogic.computeThings<int,int>(Logic::strat_B{})<<std::endl; //outputs 1
    return 0;
}

A more idiomatic and structured approach to the strategy pattern would be to lift the behaviour of the different strategies out of the computeThings function and into the strategy classes themselves:

class Logic {
public:
    struct strat_A {
        template <class A, class B>
        static int computeThings(Logic* self);
    };
    struct strat_B {
        template <class A, class B>
        static int computeThings(Logic* self);
    };

    template <class A, class B, class Strategy>
    int computeThings() {
        return Strategy::template computeThings<A, B>(this);
    }
};

template <class A, class B>
int Logic::strat_A::computeThings(Logic* self) {
    return 0;
}

template <class A, class B>
int Logic::strat_B::computeThings(Logic* self) {
    return 1;
}

int main() {
    Logic mylogic;
    std::cout<<mylogic.computeThings<int,int,Logic::strat_A>()<<std::endl; //outputs 0
    std::cout<<mylogic.computeThings<int,int,Logic::strat_B>()<<std::endl; //outputs 1
    return 0;
}

The Logic* self pointer isn't needed in this example, but would be if the strategies need to access the Logic instance.

Funnyman answered 3/3, 2019 at 18:8 Comment(4)
strategy==Logic::strat_A and strategy==Logic::strat_B are neither equivalent (two function definitions containing the expressions would not satisfy the ODR) nor functionally equivalent (evaluation doesn't always result in the same value), so I don't think the standard quote applies here. Besides, GCC's problem is with matching the out-of-class definitions to the in-class declarations, and that should work, because those pairs are actually equivalent. I remember seeing this kind of matching failure before, on GCC and MSVC; I vote for compiler bug.Turro
Nice answer, just a comment for other readers (as I’m often a proponent of tag dispatch). I don’t think we really see it’s neatness of tag dispatch here, as the dispatch is placed on the public API rather than as an implementation detail. Typically, from a public API call site, tag dispatch looks just the same as your final example; specifying detail behavior using a type (for tag or static dependency injection, as your final example) or non-type (just a tag) template parameter. The only differences between the two are entirely hidden from the public API. ...Grubb
... As the API user will simple provide a compile-time choice. Also, neither the computeThings() of the nested structs nor the tag dispatch overloads need (nor should, imho) have public visibility, as they should only ever be called from within the Logic class itself. Moreover, with the static dependency injection approach (as it is currently), API users could inject their own computeThings() implementations into the public API! If this class is just internal to some module, this could actually be useful for tests and mocking, but if this is not intended, I would suggest...Grubb
... using SFINAE to limit possible injections, or just using a controllable tag dispatch approach instead. Finally, I’ll mention that it can also be nice to use default template arguments to control which strategy/overload/injection that should be used, were the call site not to explicitly specify the tag/strategy.Grubb

© 2022 - 2024 — McMap. All rights reserved.