`enable_if` 与 `enum` 模板专业化问题

`enable_if` with `enum` template specialization problem

我在 GCC 编译 enable_ifs 时遇到问题应用于模板化 class 方法的 return 值。使用 Clang,我可以在 enum 模板参数上使用 enable_if 中的表达式,而 GCC 拒绝编译此代码。

这是问题描述、初始代码及其后续修改,试图让我和编译器满意(不幸的是,不是同时)。

我有一个非模板化的 class Logic,它包含一个模板化的 class 方法 computeThings(),它有一个 enum Strategy 作为 其模板参数之一computeThings() 中的逻辑取决于编译时 Strategy,因此 if constexpr 是一种合理的实现方式。

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

变体 1 工作正常并且可以在 clang 和 gcc 中编译。但是,我想摆脱 if constexpr 并根据选择的 StrategycomputeThings() 拆分为两个专门的方法。原因:该函数性能要求高,代码量大

因此,我想出了变体 2,它使用 enable_if 应用于 return 值。

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

我对变体 2 非常满意(尽管也希望得到反馈)。这段代码使用 AppleClang(通常可能还有 Clang)可以很好地编译并产生正确的结果。但是,它无法使用 GCC 进行编译,并出现以下错误(+ 相同但对于其他方法):

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

所以,显然,使用简单的 strategy==Logic::strat_A 与 GCC 冲突。因此,我想出了一个同时满足 clang 和 gcc 的解决方案,它将 strategy==Logic::strat_A 包装成 struct:

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

对于变体 3,Clang 和 GCC 都很满意。但是,我不是,因为出于未知原因我必须创建很多虚拟包装器(在这里,我只有一个,但从技术上讲,我应该同时拥有 isStratA<>isStratB<>)。

问题:

(如果重要,GCC 7.4.0 和 Apple LLVM 版本 10.0.0:clang-1000.11.45.5)

正如@bogdan 在评论中所说,这很可能是一个编译器错误。实际上我注意到如果你在函数模板的外联定义中使用尾随 return 类型它会起作用:

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

我更喜欢将 enable_if 放在具有默认参数的非类型模板参数的类型中:

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

但是 SFINAE 的功能对于如此简单的东西来说太复杂了。有更简单的方法来做你想做的事情。以使用标签调度为例:

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

这可以通过去掉枚举并直接定义一些标签类型来进一步简化:

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

策略模式的一种更惯用和结构化的方法是将不同策略的行为从 computeThings 函数中提升到策略 类 本身:

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

此示例中不需要 Logic* self 指针,但如果策略需要访问 Logic 实例,则需要。