Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

`enable_if` with `enum` template specialization problem

I have problems with GCC compiling enable_ifs applied to 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 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)

like image 816
Anton Menshov Avatar asked Feb 28 '19 00:02

Anton Menshov


1 Answers

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.

like image 102
Oktalist Avatar answered Oct 20 '22 09:10

Oktalist