如何在C++中无分支地根据条件调用两个模板成员函数之一
问题描述
现有一个包含两个成员模板函数fun1()、fun2()的类Foo,希望根据类的const bool成员willCallFun1的值决定调用其中一个函数。当前实现的fun()函数每次调用都会执行if-else分支判断,但willCallFun1在Foo对象构造时就已确定,因此希望消除该分支。
若fun1、fun2为非模板函数,可在类中添加函数指针或std::function成员并在构造时绑定目标函数,但针对模板函数暂无解决办法,寻求解决方案。
原始代码如下:
class Foo { public: const bool willCallFun1; Foo(bool b) : willCallFun1{b}{ } template<typename T> void fun1(T t) { std::cout << "fun1 "<< t << "\n"; } template<typename T> void fun2(T t) { std::cout << "fun2 "<< t << "\n"; } template<typename T> void fun(T t){ if (willCallFun1) fun1<T>(t); else fun2<T>(t); } }; int main() { Foo f1{true}; f1.fun(123); f1.fun("hi"); Foo f2{false}; f2.fun(456); f2.fun("hello"); }
解决方案
方案一:将willCallFun1转为类模板参数(C++17+)
把willCallFun1从类成员变量改成类模板的非类型参数,配合if constexpr在编译期直接剔除无用分支,完全消除运行时判断。
修改后的代码:
template<bool WillCallFun1> class Foo { public: template<typename T> void fun1(T t) { std::cout << "fun1 "<< t << "\n"; } template<typename T> void fun2(T t) { std::cout << "fun2 "<< t << "\n"; } template<typename T> void fun(T t){ if constexpr (WillCallFun1) { fun1<T>(t); } else { fun2<T>(t); } } }; int main() { Foo<true> f1; f1.fun(123); f1.fun("hi"); Foo<false> f2; f2.fun(456); f2.fun("hello"); }
优缺点:实现最简单,无任何运行时开销;但不同WillCallFun1值对应不同的类类型,无法直接放到同一容器中(需用多态或变体包装)。
方案二:标签分发(Tag Dispatch,C++11+)
如果不想修改类的非模板特性,可通过标签分发在编译期选择对应函数重载。核心是让willCallFun1成为编译期常量,再用std::conditional_t选择标签类型,触发对应重载。
修改后的代码:
#include <type_traits> #include <iostream> class Foo { public: constexpr bool willCallFun1; constexpr Foo(bool b) : willCallFun1{b}{ } template<typename T> void fun1(T t) { std::cout << "fun1 "<< t << "\n"; } template<typename T> void fun2(T t) { std::cout << "fun2 "<< t << "\n"; } private: // 定义用于分发的标签类型 struct Fun1Tag {}; struct Fun2Tag {}; // 针对不同标签的实现重载 template<typename T> void fun_impl(T t, Fun1Tag) { fun1<T>(t); } template<typename T> void fun_impl(T t, Fun2Tag) { fun2<T>(t); } public: template<typename T> void fun(T t){ // 编译期选择标签,触发对应重载 fun_impl(t, std::conditional_t<willCallFun1, Fun1Tag, Fun2Tag>{}); } }; int main() { constexpr Foo f1{true}; f1.fun(123); f1.fun("hi"); constexpr Foo f2{false}; f2.fun(456); f2.fun("hello"); }
优缺点:保留原类的非模板特性,对象类型统一;但要求willCallFun1是constexpr(编译期可知),如果willCallFun1只能在运行时确定,此方法不适用。
方案三:继承+CRTP(奇异递归模板模式,C++11+)
通过CRTP将不同的行为逻辑封装到基类中,派生类继承对应基类来获得目标行为,既消除运行时分支,又可通过工厂函数统一对外接口。
修改后的代码:
#include <iostream> // 基类模板,封装fun的选择逻辑 template<bool WillCallFun1, typename Derived> class FooBase { protected: template<typename T> void fun(T t) { if constexpr (WillCallFun1) { static_cast<Derived*>(this)->template fun1<T>(t); } else { static_cast<Derived*>(this)->template fun2<T>(t); } } }; // 对应willCallFun1=true的派生类 class FooTrue : public FooBase<true, FooTrue> { public: template<typename T> void fun1(T t) { std::cout << "fun1 "<< t << "\n"; } template<typename T> void fun2(T t) { std::cout << "fun2 "<< t << "\n"; } // 暴露基类的fun函数 using FooBase<true, FooTrue>::fun; }; // 对应willCallFun1=false的派生类 class FooFalse : public FooBase<false, FooFalse> { public: template<typename T> void fun1(T t) { std::cout << "fun1 "<< t << "\n"; } template<typename T> void fun2(T t) { std::cout << "fun2 "<< t << "\n"; } using FooBase<false, FooFalse>::fun; }; // 工厂函数,统一创建对象接口 template<bool B> auto makeFoo() { if constexpr (B) return FooTrue{}; else return FooFalse{}; } int main() { auto f1 = makeFoo<true>(); f1.fun(123); f1.fun("hi"); auto f2 = makeFoo<false>(); f2.fun(456); f2.fun("hello"); }
优缺点:可通过工厂函数提供统一的创建接口,同时消除运行时分支;缺点是需要定义多个派生类,代码量稍大。
内容的提问来源于stack exchange,提问作者Wayne Tseng

