带有enum模板专门化问题的“ enable_if”

时间:2019-02-28 00:14:46

标签: c++ templates gcc c++17 enable-if

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

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

我有一个非模板类Logic,其中包含模板类方法computeThings(),该方法具有enum Strategy作为模板参数的 one 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并根据所选的computeThings()Strategy分成两个专门的方法。原因:该函数对性能至关重要,并且包含大量代码。

因此,我提出了将enable_if应用于返回值的变体2。

变种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<>)。

问题:

  • 我的Variant 2是否违反任何C ++标准(或常识)?
  • 我是否有一种简单的方法可以使Variant 2型解决方案正常工作,而无需像Variant 3中那样使用虚拟包装器?

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

2 个答案:

答案 0 :(得分:4)

正如@bogdan在评论中所说,这很可能是编译器错误。实际上,我注意到,如果您在函数模板的脱机定义中使用尾随返回类型,那么它将起作用:

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实例,则将是

答案 1 :(得分:1)

TLDR :示例2和3 可能格式错误。

解决方案?拆分计算逻辑和数据,然后使用标签分发或将逻辑转换为类模板,并为每种策略定义不同的显式专业化。


enable_if用于通过利用[over.match.funcs]/7[temp.deduct]/8规则从过载集中消除过载。

[over.match.funcs]/7

  

在每种情况下,候选都是功能模板时,都会使用模板参数推导([temp.over][temp.deduct])生成候选功能模板专业化。如果构造函数模板或转换函数模板具有其常量表达式与值相关的显式说明符([temp.dep]),则首先执行模板参数推导,然后,如果上下文需要不显式的候选,则执行生成的专业化是显式的([dcl.fct.spec]),它将从候选集中删除。 ...

constructor一词并不表示类构造函数,它是一种内部编译器机制,会产生重载。 [over.match.funcs]/1

[temp.deduct]/8

  

如果替换导致无效的类型或表达式,则类型推导失败。如果使用替换的参数编写,则无效的类型或表达式将是格式错误的,并且需要诊断。 [注意:如果不需要诊断,则程序仍然格式错误。访问检查是替代过程的一部分。 — 尾注] 仅在函数类型,其模板参数类型及其显式说明符 的紧接上下文中无效的类型和表达式 strong>可能导致推论失败。 [注意: 替换为类型和表达式可能会产生效果,例如实例化类模板专业化和/或功能模板的专业化,隐式定义的函数的生成等。这种影响不在“即时上下文”中,可能导致程序格式错误。 — 尾注] ...

有一个问题,因为该标准未定义immediate context是什么。 DR1844  What exactly is the immediate context?

但是[temp.deduct.call]/5并没有推导strategy模板参数。

  

如果模板参数未在功能模板的任何功能参数中使用,或仅在非推导上下文中使用,则不能从函数调用和模板参数推导其对应的模板参数必须明确指定。

我认为这使替换发生在即时上下文之外,这使您的第二个和第三个示例格式错误。