【问题标题】:`enable_if` with `enum` template specialization problem带有“enum”模板专业化问题的“enable_if”
【发布时间】:2019-02-28 00:14:45
【问题描述】:

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

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

我有一个非模板类Logic,其中包含一个模板类方法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 应用于返回值。

变体 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&lt;(strategy == Logic:: strat_A),int&gt; Logic::computeThings()' does not match any in class 'Logic' Logic::computeThings()

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

candidates are: template&lt;class A, class B, Logic::Strategy strategy&gt; std::enable_if_t&lt;(strategy == strat_A), int&gt; 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&lt;&gt;isStratB&lt;&gt;)。

问题:

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

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

【问题讨论】:

  • @NikitaKniazev 不完全确定在给定的上下文中会导致什么。
  • 我仍然不相信这是“更简单的调用”。无论如何,你可以只用一个包装器isStratgodbolt.org/z/fXNTWP 编辑变体 3:它通过 gcc,但在 MSVC 上仍然失败,正如@NikitaKniazev 指出的那样
  • 您将“性能关键”作为此更改的原因。您是否进行过测试以查看性能是否有所提升? (如果编译器已经在幕后做了与此等效的操作,那么这可能是无益的大量工作。)
  • 但是变体 1 中没有额外的运行时 if 条件...只有编译时 if 条件。
  • @AntonMenshov 我会尝试将代码分解为几个(新)函数,但从变体 1 的 computeThings() 中现有的 if-else 框架调用这些函数。理论上,该模板的每个实例化都将简化为一个函数调用,编译器可以内联。单独的函数,没有额外的模板魔法,以及相同的开销。 (但不是关于为什么变体 2 有时有效/无效的答案。)

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


【解决方案1】:

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

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 实例,则需要。

【讨论】:

  • strategy==Logic::strat_Astrategy==Logic::strat_B 既不等价(包含表达式的两个函数定义不满足 ODR)也不在功能上等价(评估并不总是产生相同的值),所以我不'认为标准报价在这里不适用。此外,GCC 的问题在于将类外定义与类内声明相匹配,这应该可行,因为这些对实际上是等价的。我记得以前在 GCC 和 MSVC 上看到过这种匹配失败;我投票支持编译器错误。
  • 不错的答案,只是对其他读者的评论(因为我经常支持标签调度)。我认为我们在这里并没有真正看到标签调度的简洁性,因为调度被放置在公共 API 上,而不是作为实现细节。通常,从一个公共 API 调用站点,标签调度看起来和你的最后一个例子一样;使用类型(用于标记或静态依赖注入,作为最后一个示例)或非类型(只是一个标记)模板参数指定详细行为。两者之间的唯一区别对公共 API 完全隐藏。 ...
  • ... API 用户将简单地提供编译时选择。此外,嵌套结构的computeThings() 和标记调度重载都不需要(恕我直言)不应该具有public 可见性,因为它们只能从Logic 类本身中调用。此外,使用静态依赖注入方法(目前如此),API 用户可以将他们自己的computeThings() 实现注入到公共 API 中!如果这个类只是某个模块的内部,这实际上对测试和模拟很有用,但如果这不是故意的,我建议......
  • ... 使用 SFINAE 来限制可能的注入,或者只是使用可控的标签调度方法。最后,我会提到,如果调用站点没有明确指定标记/策略,使用默认模板参数来控制应该使用哪种策略/重载/注入也是很好的。
猜你喜欢
  • 2015-06-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多