如何编写支持类型及子类的Expression Templates的operator+=?
operator+=支持子类的问题 你遇到的是模板类型系统里一个典型的限制:模板类的实例化类型不会因为模板参数的继承关系自动兼容。你原本写的operator+=只接受BinaryExpr<double, Variable, plus<double>>,但3.0 + y生成的是BinaryExpr<double, SpecialVariable, plus<double>>——这两个是完全独立的类型,编译器不会因为SpecialVariable继承自Variable就自动转换它们的容器类型BinaryExpr。
下面提供几个逐步升级的解决方案:
方案1:模板化operator+=并约束参数为Variable的派生类(C++20+)
最简洁的方式是用C++20的**概念(Concepts)**来约束模板参数,让operator+=接受任何继承自Variable的类型作为BinaryExpr的第二个模板参数:
首先需要包含<concepts>头文件,然后修改Variable的operator+=为模板成员函数:
#include <concepts> class Variable { public: Variable(double& v) : _val(v) {} double eval() const { return _val; } void operator+=(double x) { _val += x; } // 模板化的operator+=,约束ExprT2必须是Variable的派生类 template <typename ExprT2> requires std::derived_from<ExprT2, Variable> void operator+=(BinaryExpr<double, ExprT2, plus<double>> expr) { _val += 1000.0; // 这里可以替换为expr.eval()来实际计算表达式值 } private: double& _val; };
原理
std::derived_from<ExprT2, Variable>会检查ExprT2是否是Variable的子类(包括自身),只有满足条件时,这个模板函数才会参与重载决议,完美匹配SpecialVariable的情况。
方案2:用SFINAE兼容C++17及更早版本
如果你的项目还不能用C++20,就用SFINAE(Substitution Failure Is Not An Error)技术,借助<type_traits>里的工具:
#include <type_traits> class Variable { public: // ... 其他成员 ... template <typename ExprT2> // 只有当ExprT2是Variable的派生类时,这个函数才会被实例化 typename std::enable_if<std::is_base_of<Variable, ExprT2>::value>::type operator+=(BinaryExpr<double, ExprT2, plus<double>> expr) { _val += 1000.0; } };
原理
std::is_base_of<Variable, ExprT2>::value在ExprT2是Variable或其子类时返回true,std::enable_if会让函数的返回类型仅在条件为true时有效,否则这个重载会被编译器忽略,不会报错。
方案3:进一步通用化,支持任意可求值的表达式
其实Expression Templates的核心优势就是延迟计算,我们可以让operator+=接受任何能调用eval()方法的表达式,而不仅仅是特定的BinaryExpr类型,这样扩展性更强:
C++20版本(用概念):
template <typename Expr> requires requires(const Expr& e) { { e.eval() } -> std::convertible_to<double>; } void operator+=(Variable& var, const Expr& expr) { var._val += expr.eval(); }
C++17及更早版本(用SFINAE):
template <typename Expr> typename std::enable_if_t<std::is_convertible_v<decltype(std::declval<const Expr>().eval()), double>> void operator+=(Variable& var, const Expr& expr) { var._val += expr.eval(); }
这样不管是Literal、Variable、SpecialVariable,还是它们组合出来的任何BinaryExpr,只要能调用eval()返回double,都可以用这个operator+=,完全解决了类型兼容问题,也更符合Expression Templates的设计思路。
测试验证
修改后运行你的测试代码:
int main(int argc, char const* argv[]) { double xd = 2.0, yd = 5.0; Variable x{xd}; SpecialVariable y{yd}; x += 3.0 + y; std::cout << "result : " << std::to_string(x.eval()) << "\n"; return 0; }
如果用方案1/2里的_val +=1000.0,输出会是1002.0;如果用方案3里的expr.eval(),因为y.eval()返回-1000.0,所以3.0 + (-1000.0) = -997.0,最终x的值是2.0 + (-997.0) = -995.0。
内容的提问来源于stack exchange,提问作者Nibor

