CppCon 2023 学习:Symbolic Calculus for High-performance Computing From Scratch Using C++23
1. 引言:希望在 C++ 中直接输入和处理数学公式
演讲开始列出了几个经典的数学公式:
- 二阶线性常微分方程(阻尼振动):
d2xdt2+2ζω0dxdt+ω02x=0 \frac{d^2x}{dt^2} + 2 \zeta \omega_0 \frac{dx}{dt} + \omega_0^2 x = 0 dt2d2x+2ζω0dtdx+ω02x=0 - 万有引力:
F=Gm1m2r2 F = G \frac{m_1 m_2}{r^2} F=Gr2m1m2 - 正弦函数表达式:
y(t)=asin(ωt+ϕ) y(t) = a \sin(\omega t + \phi) y(t)=asin(ωt+ϕ)
演讲主题:
“在 C++ 中能直接输入、操作数学公式不是很棒吗?”
这就是本次演讲的核心目标:把 C++ 的语法抽象到可以像写公式一样自然地操作符号表达式。
2. 前提与历史背景
演讲提到 2019 年的工作:
- EDSL Infinity Wars: 主流化符号计算
- 关键思想是 无状态表达式模板(Stateless Expression Templates)
区别:
| 旧式 Expression Templates | Stateless Expression Templates |
|---|---|
| 表达式模板是有状态的,终端符号由数据(向量、矩阵、张量)表示 | 公式是无状态的,数据被注入公式中 |
| 数据驱动表达式 | 公式驱动数据 |
| 演讲的进步: |
- 利用 C++20/23 的新特性 可以写出更优雅的接口。
- 目标是 从你希望输入的公式出发,设计抽象(逆向工程语言)。
3. Lambda 技巧生成唯一类型的符号
想法:
symbol a;
symbol w;
symbol t;
symbol phi;
- 每个符号都应该有一个唯一类型。
- 问题是如何生成唯一类型。
解决方法:使用 Lambda 生成默认模板参数
template <auto = []{}>
struct symbol {};
- 每次声明
symbol a;会生成不同的类型,因为每个 lambda 表达式的类型唯一。 - 验证:
symbol x;
symbol y;
std::is_same_v<decltype(x), decltype(y)>; // false
解释:每个 lambda 声明都是一个全新的类型,从而生成唯一类型的符号。
4. 符号的比较
- 对于公式重排,需要符号可以比较。
- 不能直接用
std::type_info::hash_code,因为它不是constexpr。 - 方案:为每个符号生成唯一标识符(singleton lambda 的地址)。
template <class>
struct symbol_id {
static constexpr auto singleton = []{};
static constexpr const void* address = std::addressof(singleton);
};
template <class Lhs, class Rhs>
constexpr std::strong_ordering operator<=>(symbol_id<Lhs>, symbol_id<Rhs>) {
return std::compare_three_way{}(
symbol_id<Lhs>::address,
symbol_id<Rhs>::address
);
}
- 解释:
singleton只是为了获取地址。- 用
std::compare_three_way(C++20 的<=>运算符)实现全序比较。
5. 符号绑定机制
目标:
formula f = a * sin(w * t + phi);
double y = f(a = 5.0, w = 2.5, t = 1.6, phi = 0);
operator=返回一个 binder 对象,将符号绑定到具体值。
最小化 binder 示例
template <class Symbol, class T>
struct symbol_binder {
constexpr symbol_binder(Symbol, T x) : value(x) {}
static constexpr Symbol symbol = {};
T value;
};
template <auto = []{}>
struct symbol {
template <class T>
constexpr symbol_binder<symbol, T> operator=(T value) const {
return symbol_binder(*this, value);
}
};
6. 优化 binder 避免不必要的复制
- 问题:binder 复制数据,如果是向量或矩阵,效率低。
- 策略:
- 去掉引用类型(lvalue/rvalue)
- 给非引用类型加
const
- 工具模板:
template <class T>
struct remove_lvalue_reference { ... };
template <class T>
struct remove_rvalue_reference { ... };
template <class T>
struct requalify_as_const { ... };
template <class Symbol, class T>
struct symbol_binder {
using symbol_type = Symbol;
using value_type = std::remove_cvref_t<T>;
private:
requalify_as_const_t<remove_rvalue_reference_t<T>> value;
public:
template <class U>
requires std::is_convertible_v<U&&, requalify_as_const_t<remove_rvalue_reference_t<T>>>
constexpr symbol_binder(Symbol, U&& x) noexcept(...) : value(std::forward<U>(x)) {}
const value_type& operator()() const noexcept { return value; }
};
- 绑定操作最终形式:
template <auto = []{}>
struct symbol {
template <class T>
constexpr symbol_binder<symbol, T&&> operator=(T&& value) const {
return symbol_binder(*this, std::forward<T>(value));
}
};
7. 总结
- Lambda trick:利用每个 lambda 的唯一类型生成唯一符号类型。
- 符号比较:利用 singleton 地址 +
<=>完全序比较符号。 - 绑定值:利用
symbol_binder将符号与值绑定,支持传入任意类型,同时避免不必要的复制。 - 这样就可以在 C++ 中自然书写数学公式,并进行符号计算:
symbol a, w, t, phi;
formula f = a * sin(w * t + phi);
double y = f(a = 5.0, w = 2.5, t = 1.6, phi = 0);
1. 带约束的符号(Symbols with Constraints)
目标
希望能够在声明符号时提供数学语义信息,例如:
symbol a;
symbol w;
symbol t;
symbol phi;
改成:
symbol<real> a;
symbol<real> w;
symbol<real> t;
symbol<real> phi;
动机:
- 为用户提供上下文信息,让代码可读性更高。
- 避免不期望的类型转换。
- 利用数学知识进行优化(例如,某些操作的交换律、结合律可以简化表达式或优化计算)。
演讲提出的核心思想是:给符号实现一种符号概念(symbolic concept)机制。
2. 使用 Lambda 实现约束机制
C++23 的限制:
- Concepts 不能直接作为模板参数。
解决方案:使用 Lambda 或函数对象检测类型。
struct unconstrained {
template <class T>
constexpr std::true_type operator()(T x) const noexcept { return {}; }
};
struct real {
template <class T>
constexpr std::false_type operator()(T x) const noexcept { return {}; }
template <class T>
requires std::is_floating_point_v<T>
constexpr std::true_type operator()(T x) const noexcept { return {}; }
};
template <class T = unconstrained, auto = []{}>
struct symbol {
template <class Arg>
requires decltype(std::declval<T>()(std::declval<Arg>()))::value
constexpr symbol_binder<symbol, Arg&&> operator=(Arg&& arg) const {
return symbol_binder(*this, std::forward<Arg>(arg));
}
};
解释:
unconstrained表示符号不受限制。real表示符号只能绑定浮点类型。operator=中的requires条件保证只有符合约束的类型才能绑定。
3. Trait 基机制
另一种方式是使用 traits:
template <class T>
struct unconstrained: std::true_type {};
template <class T>
struct real: std::is_floating_point<T> {};
template <template <class...> class Trait = unconstrained, auto = []{}>
struct symbol {
template <class Arg>
requires Trait<std::remove_cvref_t<Arg>>::value
constexpr symbol_binder<symbol, Arg&&> operator=(Arg&& arg) const {
return symbol_binder(*this, std::forward<Arg>(arg));
}
};
- 使用 traits 可以更方便地在编译期约束类型。
- 限制:C++ 目前支持类型泛型,但不支持“kind 泛型”,即模板参数的种类不能混合(type、NTTP、模板模板参数)。
4. 将 traits 包装成类型
解决“kind 泛型缺失”问题:
struct unconstrained {
template <class T>
struct trait: std::true_type {};
};
struct real {
template <class T>
struct trait: std::is_floating_point<T> {};
};
template <class Trait = unconstrained, auto = []{}>
struct symbol {
template <class Arg>
requires Trait::template trait<std::remove_cvref_t<Arg>>::value
constexpr symbol_binder<symbol, Arg&&> operator=(Arg&& arg) const {
return symbol_binder(*this, std::forward<Arg>(arg));
}
};
解释:
- Trait 类型内部包含一个嵌套
trait模板,用于判断给定类型是否符合约束。 symbol使用 Lambda 生成唯一类型的 ID,并结合 trait 约束。
5. 综合前面技术:最终 symbol 模板
template <
class Trait = unconstrained,
auto Id = symbol_id<decltype([]{})>{}
> struct symbol {
// 唯一标识符
static constexpr auto id = Id;
// 绑定机制
template <class Arg>
requires Trait::template trait<std::remove_cvref_t<Arg>>::value
constexpr symbol_binder<symbol, Arg&&> operator=(Arg&& arg) const {
return symbol_binder(*this, std::forward<Arg>(arg));
}
};
- 这整合了:
- 唯一类型生成(lambda trick)
- ID 唯一标识(symbol_id)
- 绑定机制(symbol_binder)
- 类型约束(Trait)
6. 数学表达式的概念与抽象
- 符号(Symbols):
- 变量符号:ϕ,ω,...\phi, \omega, ...ϕ,ω,...
- 常量符号:π,5.6,42,...\pi, 5.6, 42,...π,5.6,42,...
- 函数符号:sin,log,...\sin, \log,...sin,log,...
- 操作符号:+,−,×+, -, \times+,−,×
- 自由变量与绑定变量:
- 自由变量(free variable):符号占位符。
- 绑定变量(bound variable):已经绑定具体值。
- 构造(Constructions):
- Term(项):数学对象,如 ω×t+ϕ\omega \times t + \phiω×t+ϕ
- Formula(公式):数学语句,如 f=a×sin(ωt+ϕ)f = a \times \sin(\omega t + \phi)f=a×sin(ωt+ϕ)
- Expression(表达式):符号序列
- Equation(方程):两个公式的相等关系,如 a×sin(ωt+ϕ)=xa \times \sin(\omega t + \phi) = xa×sin(ωt+ϕ)=x
- 额外概念:
- Subterm / Subexpression(子项/子表达式)
- Well-formed expression(符合语法的表达式)
- Arity(子项数量):unary, binary, ternary 等
7. 符号操作(Actions)
- 绑定(binding):将值附加到符号
- 替换(substitution):在表达式中用值替换符号
- 重写(rewriting):在公式中将子项替换为其他项
8. 抽象语法树(AST, Abstract Syntax Tree)
以公式:
f=a×sin(ωt+ϕ) f = a \times \sin(\omega t + \phi) f=a×sin(ωt+ϕ)
对应的 AST 表示:
×
/ \
a sin
|
+
/ \
× ϕ
/ \
ω t
- 遍历方式:
- Pre-order(前序):×(a, sin(+(×(ω,t),ϕ)))
- In-order(中序):a × sin(ω t + ϕ)
- Post-order(后序):a, ω, t, ×, ϕ, +, sin, ×
9. 一个概念到万用概念
- 使用 type trait 标识符:
template <class>
struct is_symbolic: std::false_type {};
template <class T>
inline constexpr bool is_symbolic_v = is_symbolic<T>::value;
template <class T>
concept symbolic = is_symbolic_v<T>;
// 变量符号特化
template <class T, auto Id>
struct is_symbolic<symbol<T, Id>>: std::true_type {};
- 这样可以通过
symbolicconcept 判断一个类型是否是符号类型,便于泛型算法处理。
小结
- 带约束符号允许在编译期限定符号可绑定的类型(如
real浮点数)。 - Lambda trick + symbol_id 保证符号唯一类型和唯一标识。
- symbol_binder 负责绑定符号与值,同时避免不必要的复制。
- AST 与重写机制支持符号计算和表达式操作。
- Concepts/Traits 为符号提供类型语义约束,便于优化和类型检查。
#include <iostream>
#include <type_traits>
#include <utility>
#include <cmath>
// =====================================
// Traits(类型约束)
// =====================================
// unconstrained: 不限制绑定的类型,任何类型都可以绑定
struct unconstrained {
template <class T>
struct trait : std::true_type {};
};
// real: 只允许绑定浮点类型(float, double, long double)
struct real {
template <class T>
struct trait : std::is_floating_point<T> {};
};
// =====================================
// Unique symbol ID(符号唯一标识符)
// =====================================
// 每个符号都需要一个唯一的 ID 来比较和区分不同符号
template <class>
struct symbol_id {
static constexpr auto singleton = [] {}; // 匿名 lambda 作为唯一标记
static constexpr const void* address = std::addressof(singleton); // 获取 lambda 地址作为唯一 ID
};
// 对 symbol_id 提供三路比较运算符(C++20 支持 <=>)
template <class L, class R>
constexpr auto operator<=>(symbol_id<L>, symbol_id<R>) {
return symbol_id<L>::address <=> symbol_id<R>::address;
}
// =====================================
// symbol_binder - 符号绑定值
// =====================================
// symbol_binder 负责把一个符号和一个具体的值绑定起来
// Symbol 提供 value_type(绑定值的类型)
template <class Symbol>
struct symbol_binder {
using symbol_type = Symbol;
using value_type = typename Symbol::value_type; // 从 symbol 获取值类型
const Symbol& sym; // 绑定的符号
value_type value; // 绑定的值
// 构造函数:允许传入可以转换为 value_type 的值
template <class U>
requires std::is_convertible_v<U&&, const value_type&>
constexpr symbol_binder(const Symbol& s, U&& x) : sym(s), value(std::forward<U>(x)) {}
// 获取绑定值
constexpr const value_type& operator()() const noexcept { return value; }
};
// 提供类模板参数推导指导(CTAD),让 symbol_binder{...} 可以自动推导模板参数
template <class Sym, class U>
symbol_binder(Sym, U&&) -> symbol_binder<Sym>;
// =====================================
// symbol - 声明符号可以绑定的类型
// =====================================
template <class Trait = unconstrained, auto Id = symbol_id<decltype([] {})>{}>
struct symbol {
using value_type = double; // 明确符号的值类型,这里我们选择 double
static constexpr auto id = Id; // 符号唯一 ID
// operator= 允许将值绑定到符号上
template <class Arg>
requires Trait::template
trait<std::remove_cvref_t<Arg>>::value // 只允许满足 Trait 的类型
constexpr auto operator=(Arg&& arg) const {
return symbol_binder{*this, std::forward<Arg>(arg)}; // 返回 symbol_binder 对象
}
};
// =====================================
// Concept - 判断类型是否是符号
// =====================================
template <class T>
struct is_symbolic : std::false_type {};
// 针对 symbol 特化为 true_type
template <class Tr, auto Id>
struct is_symbolic<symbol<Tr, Id>> : std::true_type {};
// C++20 concept,symbolic 用于约束模板或静态断言
template <class T>
concept symbolic = is_symbolic<T>::value;
// =====================================
// main 函数示例
// =====================================
int main() {
// 定义四个符号变量
symbol<real> a, w, t, phi;
// 将值绑定到符号上,返回 symbol_binder
auto a_val = a = 5.0; // a -> 5.0
auto w_val = w = 2.5; // w -> 2.5
auto t_val = t = 1.6; // t -> 1.6
auto phi_val = phi = 0.0; // phi -> 0.0
// 使用绑定值计算 y
double y = a_val() * std::sin(w_val() * t_val() + phi_val());
std::cout << "y = " << y << std::endl;
// 编译期断言 a 是符号
static_assert(symbolic<decltype(a)>);
return 0;
}
1. Traits(类型约束)
struct unconstrained { ... };
struct real { ... };
unconstrained:不限制绑定类型,任何类型都可以绑定。real:只允许绑定浮点类型(float、double、long double)。- 作用:为符号绑定值提供类型检查,防止不合法类型绑定。
2. Unique symbol ID(符号唯一标识符)
template <class>
struct symbol_id { ... };
template <class L, class R>
constexpr auto operator<=>(symbol_id<L>, symbol_id<R>) { ... }
- 每个
symbol都有一个唯一的 ID(通过 lambda 地址生成)。 operator<=>提供符号比较的能力。- 作用:保证不同符号可以唯一区分、排序或比较,即使符号的名字相同。
3. symbol_binder(符号绑定器)
template <class Symbol>
struct symbol_binder { ... };
symbol_binder负责把一个符号和一个具体的值绑定在一起。- 它包含:
sym:引用符号本身value:绑定的值
operator()可以返回绑定值。- 作用:把符号从抽象的概念(symbol)变成可操作的值对象。
例:
auto a_val = a = 5.0; // a_val 是 symbol_binder, 保存 a 和 5.0
a_val(); // 返回 5.0
4. symbol(符号类型)
template <class Trait = unconstrained, auto Id = symbol_id<decltype([] {})>{}>
struct symbol { ... };
- 每个
symbol是一个可绑定值的符号类型。 value_type定义了符号可以绑定的值类型(这里固定为double)。operator=用于绑定值,返回symbol_binder。- 作用:
- 定义符号本身(如 a, w, t, phi)
- 限制符号可以绑定的值类型
- 支持符号绑定值的操作
5. Concept symbolic
template <class T>
concept symbolic = is_symbolic<T>::value;
- 判断一个类型是否是符号类型(
symbol)。 - 作用:在编译期检查类型,保证符号相关操作的类型安全。
6. main 函数示例
symbol<real> a, w, t, phi;
auto a_val = a = 5.0;
auto w_val = w = 2.5;
auto t_val = t = 1.6;
auto phi_val = phi = 0.0;
double y = a_val() * std::sin(w_val() * t_val() + phi_val());
- 定义了四个符号
a, w, t, phi。 - 将具体值绑定到符号上:
a = 5.0返回symbol_binder<symbol<real>, double>- 通过
operator()获取绑定值。
- 使用绑定值进行数学计算(计算
y = a * sin(w*t + phi))。 - 静态断言验证
a是符号类型。
总结
这个示例的主要功能是:
- 抽象数学符号(symbol)为类型。
- 为符号绑定值,返回
symbol_binder,可以安全地获取绑定值。 - 类型约束检查,保证符号绑定的值符合预期类型。
- 支持 编译期静态检查,如
static_assert(symbolic<decltype(a)>)。 - 提供基础框架,可以进一步扩展为 符号表达式计算(AST 构建、符号代数运算、公式重写等)。
auto expr = a * sin(w*t + phi);
1. 引入示例
假设我们有一个公式:
formula f = a * sin(w * t + phi);
我们希望在符号化公式中直接进行替换(substitution):
double y = f(a = 5.0, w = 2.5, t = 1.6, phi = 0);
这里的操作就是 将具体数值绑定到符号上,并计算公式的结果。
用数学形式表示就是:
f(a,w,t,ϕ)=a⋅sin(w⋅t+ϕ) f(a, w, t, \phi) = a \cdot \sin(w \cdot t + \phi) f(a,w,t,ϕ)=a⋅sin(w⋅t+ϕ)
经过替换后:
y=f(5.0,2.5,1.6,0.0)=5.0⋅sin(2.5⋅1.6+0) y = f(5.0, 2.5, 1.6, 0.0) = 5.0 \cdot \sin(2.5 \cdot 1.6 + 0) y=f(5.0,2.5,1.6,0.0)=5.0⋅sin(2.5⋅1.6+0)
2. 从公式到表达式
2.1 formula 模板
template <symbolic Expression>
struct formula {
using expression = Expression;
constexpr formula(Expression expr) noexcept {};
template <class... Args>
constexpr auto operator()(Args... args) const noexcept {
return expression{}(substitution(args...));
}
};
Expression是一个符号化表达式类型(symbolic)。operator()接受一组绑定操作Args...,返回 经过替换后的表达式的值。- 关键是调用了
substitution(args...),把绑定值打包成 substitution 对象。
2.2 substitution 基本结构
概念:一个 substitution 是 符号绑定器的集合,每个绑定器根据符号 ID 索引,可以快速查找绑定值。
(1) index_constant
template <std::size_t I>
struct index_constant : std::integral_constant<std::size_t, I> {};
template <std::size_t I>
inline constexpr index_constant<I> index = {};
- 为每个绑定器提供唯一索引,用于在 substitution 中定位。
(2) substitution_element
template <std::size_t I, Binder B>
struct substitution_element {
using index = index_constant<I>;
using id_type = decltype(B::symbol_type::id);
constexpr substitution_element(const Binder& b): _binder(b) {}
constexpr const T& operator[](index) const { return _binder; }
constexpr const T& operator[](id_type) const { return _binder; }
private:
const B _binder;
};
- 每个元素
_binder保存 符号绑定值。 - 可以通过 索引
I或 符号 ID 访问绑定值。 - 这就是 substitution 的核心魔法。
2.3 substitution_base
template <std::size_t... Index, class... Binders>
struct substitution_base<std::index_sequence<Index...>, Binders...>
: substitution_element<Index, Binders>... {
using index_sequence = std::index_sequence<Index...>;
using substitution_element<Index, Binders>::operator[]...;
constexpr substitution_base(const Binders&... x) : substitution_element<Index, Binders>(x)... {}
};
substitution_base继承所有substitution_element,从而 打包整个绑定集合。- 支持通过
operator[]访问每个绑定值。
2.4 substitution
template <class... Binders>
struct substitution : substitution_base<std::index_sequence_for<Binders...>, Binders...> {
using base = substitution_base<std::index_sequence_for<Binders...>, Binders...>;
using base::base;
using base::operator[];
};
// CTAD 推导
template <class... Binders>
substitution(const Binders&...) -> substitution<Binders...>;
- 是最终对外使用的 substitution 类型,用户可以写:
s = substitution(a=5, b=2);
std::cout << s[a.id] << std::endl; // 输出 5
- 核心作用:把多个符号绑定集合封装起来,通过符号 ID 进行查找。
3. 使用流程总结
- 定义符号:
symbol<real> a, w, t, phi;
- 创建公式:
formula f = a * sin(w * t + phi);
- 绑定值生成 substitution:
auto s = substitution(a=5.0, w=2.5, t=1.6, phi=0.0);
- 公式计算(替换绑定值):
double y = f(a=5.0, w=2.5, t=1.6, phi=0.0);
数学对应关系:
y=f(a,w,t,ϕ)经过 substitutiony=f(5.0,2.5,1.6,0.0) y = f(a, w, t, \phi) \quad\text{经过 substitution}\quad y = f(5.0, 2.5, 1.6, 0.0) y=f(a,w,t,ϕ)经过 substitutiony=f(5.0,2.5,1.6,0.0)
总结
- substitution 是符号计算系统的关键组成部分,它把符号绑定的具体值打包成集合。
- 使用 符号 ID 或 索引 可以高效访问绑定值。
formula和substitution结合,实现了 符号公式的值替换,类似数学里的函数调用。- 这套机制为进一步实现 符号表达式计算、公式重写、符号代数运算 打下基础。
如果你需要,我可以帮你写一个 完整可运行的示例,演示:
formula f = a*sin(w*t + phi);
double y = f(a=5.0, w=2.5, t=1.6, phi=0.0);
#include <iostream>
#include <cmath>
#include <type_traits>
#include <utility>
#include <numbers> // C++20 中的数学常数
// =====================================
// Traits(类型约束)
// =====================================
struct unconstrained {
// trait 模板:对所有类型都返回 true
template <class T>
struct trait : std::true_type {};
};
struct real {
// trait 模板:只对浮点类型返回 true
template <class T>
struct trait : std::is_floating_point<T> {};
};
// =====================================
// 符号唯一 ID(symbol_id)
// =====================================
// 每个符号(symbol)都需要唯一的标识符
template <class>
struct symbol_id {
static constexpr auto singleton = [] {}; // 用 lambda 生成唯一类型
static constexpr const void* address = std::addressof(singleton); // 获取唯一地址
};
// 不同符号 ID 可以比较大小
template <class L, class R>
constexpr auto operator<=>(symbol_id<L>, symbol_id<R>) {
return symbol_id<L>::address <=> symbol_id<R>::address;
}
// =====================================
// symbol_binder(符号绑定值)
// =====================================
template <class Symbol>
struct symbol_binder {
using symbol_type = Symbol; // 绑定的符号类型
using value_type = typename Symbol::value_type; // 符号的值类型
const Symbol& sym; // 符号引用
value_type value; // 符号绑定的实际值
// 构造函数:将符号和一个值绑定
template <class U>
requires std::is_convertible_v<U&&, const value_type&>
constexpr symbol_binder(const Symbol& s, U&& x) : sym(s), value(std::forward<U>(x)) {}
// 调用 operator() 返回绑定的值
constexpr const value_type& operator()() const noexcept { return value; }
};
// 辅助模板推导,可以直接用 symbol_binder(sym, value)
template <class Sym, class U>
symbol_binder(Sym, U&&) -> symbol_binder<Sym>;
// =====================================
// symbol(符号本身)
// =====================================
template <class Trait = unconstrained, auto Id = symbol_id<decltype([] {})>{}>
struct symbol {
using value_type = double; // 符号的值类型为 double
static constexpr auto id = Id; // 符号唯一 ID
// 将符号绑定一个值,返回 symbol_binder
template <class Arg>
requires Trait::template
trait<std::remove_cvref_t<Arg>>::value constexpr auto operator=(Arg&& arg) const {
return symbol_binder{*this, std::forward<Arg>(arg)};
}
};
// =====================================
// substitution system(符号替换系统)
// =====================================
// 单个符号绑定元素
template <std::size_t I, class Binder>
struct substitution_element {
using index = std::integral_constant<std::size_t, I>; // 索引类型
using id_type = decltype(Binder::symbol_type::id); // 符号 ID 类型
constexpr substitution_element(const Binder& b) : binder(b) {}
// 按索引访问绑定
constexpr const Binder& operator[](index) const { return binder; }
// 按符号 ID 访问绑定
constexpr const Binder& operator[](id_type) const { return binder; }
private:
const Binder& binder;
};
// 多个符号绑定组合
template <class Seq, class... Binders>
struct substitution_base;
template <std::size_t... I, class... Binders>
struct substitution_base<std::index_sequence<I...>, Binders...>
: substitution_element<I, Binders>... { // 继承每个 substitution_element
using substitution_element<I, Binders>::operator[]...; // 继承 operator[]
constexpr substitution_base(const Binders&... bs) : substitution_element<I, Binders>(bs)... {}
};
// 对外使用的 substitution 类型
template <class... Binders>
struct substitution : substitution_base<std::index_sequence_for<Binders...>, Binders...> {
using base = substitution_base<std::index_sequence_for<Binders...>, Binders...>;
using base::base;
using base::operator[];
};
// 辅助模板推导
template <class... Binders>
substitution(Binders...) -> substitution<Binders...>;
// =====================================
// formula(表达式封装)
// =====================================
template <class Expr>
struct formula {
Expr expr; // 内部表达式对象
constexpr formula(Expr e = {}) : expr(e) {}
// 调用公式,传入多个符号绑定
template <class... Binders>
constexpr auto operator()(Binders... bindings) const {
auto subst = substitution(bindings...); // 构建 substitution
return expr(subst); // 调用表达式求值
}
};
// =====================================
// 全局符号实例(关键!保证唯一性)
// =====================================
inline constexpr symbol<real> a; // 符号 a
inline constexpr symbol<real> w; // 符号 w
inline constexpr symbol<real> t; // 符号 t
inline constexpr symbol<real> phi; // 符号 phi
// =====================================
// 表达式 example_expr
// =====================================
struct example_expr {
template <class Subst>
constexpr double operator()(Subst s) const {
// 使用全局符号的 .id 查找绑定值
auto a_val = s[a.id](); // 获取 a 的值
auto w_val = s[w.id](); // 获取 w 的值
auto t_val = s[t.id](); // 获取 t 的值
auto phi_val = s[phi.id](); // 获取 phi 的值
// 计算公式 y = a * sin(w * t + phi)
return a_val * std::sin(w_val * t_val + phi_val);
}
};
// =====================================
// main 函数
// =====================================
int main() {
formula f(example_expr{}); // 将表达式封装为 formula
// 调用 formula,传入符号绑定值
double y = f(a = 5.0, w = 2.5, t = 1.6, phi = 0.0);
std::cout << "y = " << y << std::endl; // 输出结果: y ≈ 2.10701
return 0;
}
1. 核心概念
- stateless expression templates(无状态表达式模板)
- 所有表达式模板都是无状态的,不保存具体值,值是在之后通过绑定注入。
- 优点:可以构建任意复杂表达式 AST(抽象语法树),支持符号计算。
- lambda trick(lambda 技巧)
- 利用 lambda 创建唯一类型并结合默认模板参数生成符号唯一 ID。
- 核心思想:每个符号声明都生成一个唯一类型,通过地址或类型比较实现唯一性。
- symbolic
- 表示任何符号表达式的概念(concept)。
- 例如
symbol、constant_symbol、symbolic_expression都是symbolic。
- symbol
- 声明一个符号变量。
- 示例:
symbol a; - 可以绑定值:
a = 5.0;
- symbol_id
- 为每个符号生成唯一 ID,用于 substitution 系统中索引和比较。
- symbol_binder
- 将一个符号绑定到具体数值,形成“符号 → 值”的映射。
- symbol_constraint
- 对符号类型进行约束,例如只能绑定浮点数
real。
- 对符号类型进行约束,例如只能绑定浮点数
- constant_symbol
- 表示符号常量,例如 π\piπ 或数学常数:
template <auto Value> struct constant_symbol { using type = decltype(Value); static constexpr type value = Value; }; - 对应操作:
template <auto Value> struct is_symbolic<constant_symbol<Value>> : std::true_type {};
- 表示符号常量,例如 π\piπ 或数学常数:
2. 符号表达式构建
- symbolic_expression
- 抽象语法树(AST)节点,存储操作符和操作数:
template <class Operator, symbolic... Terms> struct symbolic_expression {};
- 抽象语法树(AST)节点,存储操作符和操作数:
- 使其成为符号表达式:
template <class Operator, symbolic... Terms> struct is_symbolic<symbolic_expression<Operator, Terms...>> : std::true_type {}; - 运算符重载
- 使表达式像普通数学式一样书写:
template <symbolic Lhs, symbolic Rhs> constexpr symbolic_expression<std::plus<void>, Lhs, Rhs> operator+(Lhs, Rhs) noexcept { return {}; } template <symbolic Lhs, symbolic Rhs> constexpr symbolic_expression<std::multiplies<void>, Lhs, Rhs> operator*(Lhs, Rhs) noexcept { return {}; }
- 使表达式像普通数学式一样书写:
- 自定义函数符号
- 定义函数对象:
struct sin_symbol { template <class Arg> constexpr auto operator()(Arg&& arg) { return std::sin(std::forward<Arg>(arg)); } }; template <symbolic Arg> constexpr symbolic_expression<sin_symbol, Arg> sin(Arg) noexcept { return {}; }
- 定义函数对象:
- 构建公式
- 组合符号和运算符:
formula f = a * sin(w * t + phi);
- 组合符号和运算符:
3. substitution(符号替换系统)
- 核心思想
- 将符号表达式中的每个符号替换成具体数值,从而计算表达式结果。
- 对符号:
template <class Trait = unconstrained, auto Id = symbol_id<decltype([]{})>{}> struct symbol { template <class... Binders> constexpr auto operator()(const substitution<Binders...>& s) const { return s[id](); // 通过 ID 获取绑定值 } }; - 对常量:
template <auto Value> struct constant_symbol { template <class... Binders> constexpr type operator()(const substitution<Binders...>&) const { return value; } }; - 对表达式:
template <class Operator, symbolic... Terms> struct symbolic_expression { template <class... Binders> constexpr auto operator()(const substitution<Binders...>& s) const noexcept { return Operator{}(Terms{}(s)...); // 递归计算 } };
- 最终计算公式
double y = f(a = 5.0, w = 2.5, t = 1.6, phi = 0);
4. 数学公式示例
- 表达式:
y=a⋅sin(w⋅t+ϕ) y = a \cdot \sin(w \cdot t + \phi) y=a⋅sin(w⋅t+ϕ) - 当绑定:
a=5.0,w=2.5,t=1.6,ϕ=0 a = 5.0, \quad w = 2.5, \quad t = 1.6, \quad \phi = 0 a=5.0,w=2.5,t=1.6,ϕ=0
计算:
y≈5.0⋅sin(2.5⋅1.6+0)≈2.10701 y \approx 5.0 \cdot \sin(2.5 \cdot 1.6 + 0) \approx 2.10701 y≈5.0⋅sin(2.5⋅1.6+0)≈2.10701
5. 总结
- 关键思想
- 所有表达式模板都是无状态的,数据在调用时注入。
- lambda trick 保证符号唯一性。
- substitution 系统允许符号替换和表达式求值。
- 可扩展支持常量符号、运算符、函数、符号表达式。
- 未来扩展
- 部分替换(partial substitution)
- 表达式重写(rewrite)
- 符号计算(微分、积分)
- 高性能计算(矩阵运算、GPU 加速)
- 自定义规则化简(custom rule-based simplification)
- 哲学
- “从你想要书写的数学式出发设计抽象”
- 逆向工程语言,使得 C++ 符号表达式直观易用。
#include <iostream>
#include <type_traits>
#include <functional>
#include <cmath>
#include <memory>
#include <utility>
// ============================================================================
// 类型特征工具 - 用于引用类型的操作
// Type Traits for Reference Manipulation
// ============================================================================
// 移除左值引用(但保留右值引用)
// Remove lvalue references only, keep rvalue references
template <typename T>
struct remove_lvalue_reference : std::type_identity<T> {};
template <typename T>
requires std::is_lvalue_reference_v<T>
struct remove_lvalue_reference<T> : std::type_identity<std::remove_reference_t<T>> {};
template <typename T>
using remove_lvalue_reference_t = typename remove_lvalue_reference<T>::type;
// 移除右值引用(但保留左值引用)
// Remove rvalue references only, keep lvalue references
template <typename T>
struct remove_rvalue_reference : std::type_identity<T> {};
template <typename T>
requires std::is_rvalue_reference_v<T>
struct remove_rvalue_reference<T> : std::type_identity<std::remove_reference_t<T>> {};
template <typename T>
using remove_rvalue_reference_t = typename remove_rvalue_reference<T>::type;
// 将类型重新限定为const,同时保留引用类型
// Requalify types as const while preserving reference types
// 例如:T& -> const T&, T&& -> const T&&, T -> const T
template <typename T>
struct requalify_as_const
: std::conditional<std::is_lvalue_reference_v<T>,
std::add_lvalue_reference_t<std::add_const_t<std::remove_reference_t<T>>>,
std::conditional_t<std::is_rvalue_reference_v<T>,
std::add_rvalue_reference_t<
std::add_const_t<std::remove_reference_t<T>>>,
std::add_const_t<T>>> {};
template <typename T>
using requalify_as_const_t = typename requalify_as_const<T>::type;
// ============================================================================
// 符号约束(概念模拟)
// Symbol Constraints (Concept Simulation)
// ============================================================================
// 无约束 - 接受任何类型
// Unconstrained - accepts any type
template <typename T>
struct unconstrained : std::true_type {};
// 实数约束 - 只接受浮点类型
// Real number constraint - only accepts floating-point types
template <typename T>
struct real : std::is_floating_point<T> {};
// 整数约束 - 只接受整数类型
// Integer constraint - only accepts integral types
template <typename T>
struct integer : std::is_integral<T> {};
// 算术类型约束 - 接受任何算术类型
// Arithmetic constraint - accepts any arithmetic type
template <typename T>
struct arithmetic : std::is_arithmetic<T> {};
// ============================================================================
// 符号标识符 - 使用lambda技巧生成唯一类型
// Symbol Identifier - using the lambda trick to generate unique types
// ============================================================================
// 符号ID结构体,每个lambda都会生成一个唯一的类型
// Symbol ID struct, each lambda generates a unique type
template <auto Lambda>
struct symbol_id {
static constexpr auto singleton = Lambda; // 单例lambda
static constexpr const void* address = std::addressof(singleton); // lambda的地址
};
// 三路比较运算符 - 用于比较不同的符号ID
// Three-way comparison operator - for comparing different symbol IDs
template <auto Lambda1, auto Lambda2>
constexpr std::strong_ordering operator<=>(symbol_id<Lambda1>, symbol_id<Lambda2>) {
// 使用std::compare_three_way而非直接比较指针(避免未定义行为)
// Use std::compare_three_way instead of direct pointer comparison (avoid UB)
return std::compare_three_way{}(symbol_id<Lambda1>::address, symbol_id<Lambda2>::address);
}
// 相等比较运算符
// Equality comparison operator
template <auto Lambda1, auto Lambda2>
constexpr bool operator==(symbol_id<Lambda1>, symbol_id<Lambda2>) {
return symbol_id<Lambda1>::address == symbol_id<Lambda2>::address;
}
// ============================================================================
// 符号绑定器 - 将符号与具体值绑定
// Symbol Binder - binds symbols to concrete values
// ============================================================================
// 符号绑定器:存储符号和对应的值
// Symbol binder: stores a symbol and its corresponding value
template <typename Symbol, typename T>
struct symbol_binder {
using symbol_type = Symbol;
using value_type = std::remove_cvref_t<T>;
static constexpr Symbol symbol = {}; // 关联的符号
// 构造函数:接受符号和值
// Constructor: accepts symbol and value
template <typename U>
requires std::is_convertible_v<U, remove_rvalue_reference_t<T>>
constexpr symbol_binder(Symbol, U&& x) noexcept(
std::is_nothrow_convertible_v<U, remove_rvalue_reference_t<T>>)
: value(std::forward<U>(x)) {}
// 访问器:返回存储的值
// Accessor: returns the stored value
constexpr const value_type& operator()() const noexcept { return value; }
private:
// 存储值,使用const限定以避免不必要的拷贝
// Store value with const qualification to avoid unnecessary copies
requalify_as_const_t<remove_rvalue_reference_t<T>> value;
};
// 推导指南:自动推导模板参数
// Deduction guide: automatically deduce template parameters
template <typename Symbol, typename T>
symbol_binder(Symbol, T&&) -> symbol_binder<Symbol, T>;
// ============================================================================
// 符号 - 数学公式中的变量
// Symbol - variables in mathematical formulas
// ============================================================================
// 符号类:表示数学表达式中的变量(如x, y, z等)
// Symbol class: represents variables in mathematical expressions (like x, y, z, etc.)
// Id: 使用lambda的默认参数生成唯一类型
// Constraint: 类型约束模板,限制符号可以绑定的值的类型
template <auto Id = [] {}, template <typename> typename Constraint = unconstrained>
struct symbol {
static constexpr auto id = Id; // 符号的唯一标识
using constraint_type = Constraint<void>; // 约束类型
// 赋值运算符:创建符号绑定(符号 = 值)
// Assignment operator: creates a symbol binding (symbol = value)
template <typename Arg>
requires Constraint<std::remove_cvref_t<Arg>>::value // 检查值是否满足约束
constexpr auto operator=(Arg&& arg) const {
return symbol_binder(*this, std::forward<Arg>(arg));
}
};
// ============================================================================
// 表达式模板基类
// Expression Template Base Class
// ============================================================================
// CRTP基类:所有表达式节点的基类
// CRTP base class: base class for all expression nodes
template <typename Derived>
struct expression {
// 获取派生类的引用(CRTP模式)
// Get reference to derived class (CRTP pattern)
constexpr const Derived& derived() const noexcept { return static_cast<const Derived&>(*this); }
};
// ============================================================================
// 表达式节点类型
// Expression Node Types
// ============================================================================
// 常量节点:表示常数值
// Constant node: represents constant values
template <typename T>
struct constant : expression<constant<T>> {
T value;
constexpr explicit constant(T v) : value(v) {}
// 求值:常量的值不依赖于任何符号绑定
// Evaluation: constant value doesn't depend on any symbol bindings
template <typename... Binders>
constexpr T operator()(Binders&&...) const {
return value;
}
};
// 符号节点:表示一个符号变量
// Symbol node: represents a symbol variable
template <typename Symbol>
struct symbol_node : expression<symbol_node<Symbol>> {
static constexpr Symbol symbol = {};
// 求值:从绑定列表中查找符号对应的值
// Evaluation: find the value corresponding to the symbol from the binding list
template <typename... Binders>
constexpr auto operator()(Binders&&... binders) const {
return get_value(std::forward<Binders>(binders)...);
}
private:
// 递归查找符号对应的绑定值
// Recursively find the binding value for the symbol
template <typename First, typename... Rest>
constexpr auto get_value(First&& first, Rest&&... rest) const {
if constexpr (std::is_same_v<Symbol, typename std::remove_cvref_t<First>::symbol_type>) {
// 找到匹配的符号,返回其值
// Found matching symbol, return its value
return first();
} else {
// 继续在剩余的绑定中查找
// Continue searching in remaining bindings
return get_value(std::forward<Rest>(rest)...);
}
}
};
// 二元操作节点:表示两个操作数的运算
// Binary operation node: represents an operation on two operands
template <typename Op, typename Left, typename Right>
struct binary_op : expression<binary_op<Op, Left, Right>> {
Left left; // 左操作数
Right right; // 右操作数
constexpr binary_op(Left l, Right r) : left(l), right(r) {}
// 求值:先递归求值左右操作数,然后应用运算符
// Evaluation: recursively evaluate left and right operands, then apply operator
template <typename... Binders>
constexpr auto operator()(Binders&&... binders) const {
return Op{}(left(std::forward<Binders>(binders)...),
right(std::forward<Binders>(binders)...));
}
};
// 一元操作节点:表示单个操作数的运算
// Unary operation node: represents an operation on a single operand
template <typename Op, typename Arg>
struct unary_op : expression<unary_op<Op, Arg>> {
Arg arg; // 操作数
constexpr explicit unary_op(Arg a) : arg(a) {}
// 求值:先递归求值操作数,然后应用运算符
// Evaluation: recursively evaluate operand, then apply operator
template <typename... Binders>
constexpr auto operator()(Binders&&... binders) const {
return Op{}(arg(std::forward<Binders>(binders)...));
}
};
// ============================================================================
// 运算符函数对象
// Operation Function Objects
// ============================================================================
// 加法运算
// Addition operation
struct add_op {
template <typename T, typename U>
constexpr auto operator()(T&& a, U&& b) const {
return std::forward<T>(a) + std::forward<U>(b);
}
};
// 减法运算
// Subtraction operation
struct sub_op {
template <typename T, typename U>
constexpr auto operator()(T&& a, U&& b) const {
return std::forward<T>(a) - std::forward<U>(b);
}
};
// 乘法运算
// Multiplication operation
struct mul_op {
template <typename T, typename U>
constexpr auto operator()(T&& a, U&& b) const {
return std::forward<T>(a) * std::forward<U>(b);
}
};
// 除法运算
// Division operation
struct div_op {
template <typename T, typename U>
constexpr auto operator()(T&& a, U&& b) const {
return std::forward<T>(a) / std::forward<U>(b);
}
};
// 取负运算
// Negation operation
struct neg_op {
template <typename T>
constexpr auto operator()(T&& a) const {
return -std::forward<T>(a);
}
};
// 正弦函数
// Sine function
struct sin_op {
template <typename T>
auto operator()(T&& a) const {
return std::sin(std::forward<T>(a));
}
};
// 余弦函数
// Cosine function
struct cos_op {
template <typename T>
auto operator()(T&& a) const {
return std::cos(std::forward<T>(a));
}
};
// 指数函数
// Exponential function
struct exp_op {
template <typename T>
auto operator()(T&& a) const {
return std::exp(std::forward<T>(a));
}
};
// 平方根函数
// Square root function
struct sqrt_op {
template <typename T>
auto operator()(T&& a) const {
return std::sqrt(std::forward<T>(a));
}
};
// ============================================================================
// 运算符重载 - 构建表达式树
// Operator Overloads - building expression trees
// ============================================================================
// 加法运算符重载(表达式 + 表达式)
// Addition operator overload (expression + expression)
template <typename L, typename R>
requires std::derived_from<L, expression<L>> && std::derived_from<R, expression<R>>
constexpr auto operator+(const L& left, const R& right) {
return binary_op<add_op, L, R>(left, right);
}
// 加法运算符重载(表达式 + 数值)
// Addition operator overload (expression + numeric)
template <typename L, typename T>
requires std::derived_from<L, expression<L>> && std::is_arithmetic_v<T>
constexpr auto operator+(const L& left, T right) {
return binary_op<add_op, L, constant<T>>(left, constant<T>(right));
}
// 加法运算符重载(数值 + 表达式)
// Addition operator overload (numeric + expression)
template <typename T, typename R>
requires std::is_arithmetic_v<T> && std::derived_from<R, expression<R>>
constexpr auto operator+(T left, const R& right) {
return binary_op<add_op, constant<T>, R>(constant<T>(left), right);
}
// 减法运算符重载(表达式 - 表达式)
// Subtraction operator overload (expression - expression)
template <typename L, typename R>
requires std::derived_from<L, expression<L>> && std::derived_from<R, expression<R>>
constexpr auto operator-(const L& left, const R& right) {
return binary_op<sub_op, L, R>(left, right);
}
// 减法运算符重载(表达式 - 数值)
// Subtraction operator overload (expression - numeric)
template <typename L, typename T>
requires std::derived_from<L, expression<L>> && std::is_arithmetic_v<T>
constexpr auto operator-(const L& left, T right) {
return binary_op<sub_op, L, constant<T>>(left, constant<T>(right));
}
// 减法运算符重载(数值 - 表达式)
// Subtraction operator overload (numeric - expression)
template <typename T, typename R>
requires std::is_arithmetic_v<T> && std::derived_from<R, expression<R>>
constexpr auto operator-(T left, const R& right) {
return binary_op<sub_op, constant<T>, R>(constant<T>(left), right);
}
// 乘法运算符重载(表达式 * 表达式)
// Multiplication operator overload (expression * expression)
template <typename L, typename R>
requires std::derived_from<L, expression<L>> && std::derived_from<R, expression<R>>
constexpr auto operator*(const L& left, const R& right) {
return binary_op<mul_op, L, R>(left, right);
}
// 乘法运算符重载(表达式 * 数值)
// Multiplication operator overload (expression * numeric)
template <typename L, typename T>
requires std::derived_from<L, expression<L>> && std::is_arithmetic_v<T>
constexpr auto operator*(const L& left, T right) {
return binary_op<mul_op, L, constant<T>>(left, constant<T>(right));
}
// 乘法运算符重载(数值 * 表达式)
// Multiplication operator overload (numeric * expression)
template <typename T, typename R>
requires std::is_arithmetic_v<T> && std::derived_from<R, expression<R>>
constexpr auto operator*(T left, const R& right) {
return binary_op<mul_op, constant<T>, R>(constant<T>(left), right);
}
// 除法运算符重载(表达式 / 表达式)
// Division operator overload (expression / expression)
template <typename L, typename R>
requires std::derived_from<L, expression<L>> && std::derived_from<R, expression<R>>
constexpr auto operator/(const L& left, const R& right) {
return binary_op<div_op, L, R>(left, right);
}
// 除法运算符重载(表达式 / 数值)
// Division operator overload (expression / numeric)
template <typename L, typename T>
requires std::derived_from<L, expression<L>> && std::is_arithmetic_v<T>
constexpr auto operator/(const L& left, T right) {
return binary_op<div_op, L, constant<T>>(left, constant<T>(right));
}
// 除法运算符重载(数值 / 表达式)
// Division operator overload (numeric / expression)
template <typename T, typename R>
requires std::is_arithmetic_v<T> && std::derived_from<R, expression<R>>
constexpr auto operator/(T left, const R& right) {
return binary_op<div_op, constant<T>, R>(constant<T>(left), right);
}
// 取负运算符重载(-表达式)
// Negation operator overload (-expression)
template <typename E>
requires std::derived_from<E, expression<E>>
constexpr auto operator-(const E& expr) {
return unary_op<neg_op, E>(expr);
}
// ============================================================================
// 数学函数
// Mathematical Functions
// ============================================================================
// 正弦函数
// Sine function
template <typename E>
requires std::derived_from<E, expression<E>>
auto sin(const E& expr) {
return unary_op<sin_op, E>(expr);
}
// 余弦函数
// Cosine function
template <typename E>
requires std::derived_from<E, expression<E>>
auto cos(const E& expr) {
return unary_op<cos_op, E>(expr);
}
// 指数函数
// Exponential function
template <typename E>
requires std::derived_from<E, expression<E>>
auto exp(const E& expr) {
return unary_op<exp_op, E>(expr);
}
// 平方根函数
// Square root function
template <typename E>
requires std::derived_from<E, expression<E>>
auto sqrt(const E& expr) {
return unary_op<sqrt_op, E>(expr);
}
// ============================================================================
// 公式包装器 - 继承自expression以支持运算符重载
// Formula wrapper - inherits from expression to work with operators
// ============================================================================
// 公式类:包装表达式树,使其可以像函数一样调用
// Formula class: wraps expression tree, making it callable like a function
template <typename Expr>
struct formula : expression<formula<Expr>> {
Expr expr; // 包装的表达式树
constexpr formula(Expr e) : expr(e) {}
// 函数调用运算符:使用符号绑定求值表达式
// Function call operator: evaluate expression with symbol bindings
template <typename... Binders>
constexpr auto operator()(Binders&&... binders) const {
return expr(std::forward<Binders>(binders)...);
}
};
// 转换函数:将符号转换为符号节点(表达式)
// Conversion function: convert symbol to symbol node (expression)
template <auto Id, template <typename> typename Constraint>
constexpr auto make_expr(symbol<Id, Constraint> s) {
return symbol_node<symbol<Id, Constraint>>{};
}
// 恒等转换:表达式保持不变
// Identity conversion: expressions remain unchanged
template <typename E>
requires std::derived_from<E, expression<E>>
constexpr const E& make_expr(const E& expr) {
return expr;
}
// ============================================================================
// 示例代码 - 演示符号计算系统的使用
// Demo Code - demonstrating the symbolic calculus system
// ============================================================================
int main() {
// 示例1:简单方程 y(t) = a * sin(w * t + phi)
// Example 1: Simple equation y(t) = a * sin(w * t + phi)
std::cout << "=== 示例1:正弦波方程 ===" << std::endl;
std::cout << "=== Example 1: Sine wave equation ===" << std::endl;
symbol<[] {}, real> a; // 振幅 (amplitude)
symbol<[] {}, real> w; // 角频率 (angular frequency)
symbol<[] {}, real> t; // 时间 (time)
symbol<[] {}, real> phi; // 相位 (phase)
// 构建表达式树:a * sin(w * t + phi)
// Build expression tree: a * sin(w * t + phi)
auto f = make_expr(a) * sin(make_expr(w) * make_expr(t) + make_expr(phi));
// 求值:将具体值绑定到符号
// Evaluate: bind concrete values to symbols
double y = f(a = 5.0, w = 2.5, t = 1.6, phi = 0.0);
std::cout << "y = a * sin(w * t + phi)" << std::endl;
std::cout << " a=5.0, w=2.5, t=1.6, phi=0.0" << std::endl;
std::cout << " 结果(result) = " << y << std::endl << std::endl;
// 示例2:二次方程 x^2 + b*x + c
// Example 2: Quadratic equation x^2 + b*x + c
std::cout << "=== 示例2:二次方程 ===" << std::endl;
std::cout << "=== Example 2: Quadratic equation ===" << std::endl;
symbol<[] {}, real> x; // 自变量 (independent variable)
symbol<[] {}, real> b; // 一次项系数 (linear coefficient)
symbol<[] {}, real> c; // 常数项 (constant term)
// 构建表达式树:x*x + b*x + c
// Build expression tree: x*x + b*x + c
auto quad = make_expr(x) * make_expr(x) + make_expr(b) * make_expr(x) + make_expr(c);
double result = quad(x = 2.0, b = 3.0, c = 1.0);
std::cout << "f(x) = x^2 + b*x + c" << std::endl;
std::cout << " x=2.0, b=3.0, c=1.0" << std::endl;
std::cout << " 结果(result) = " << result << std::endl << std::endl;
// 示例3:高斯分布概率密度函数
// Example 3: Gaussian distribution probability density function
std::cout << "=== 示例3:高斯分布PDF ===" << std::endl;
std::cout << "=== Example 3: Gaussian distribution PDF ===" << std::endl;
symbol<[] {}, real> mu; // 均值 (mean)
symbol<[] {}, real> sigma; // 标准差 (standard deviation)
// 构建高斯分布公式
// Build Gaussian distribution formula
// PDF(x) = (1 / (σ * sqrt(2π))) * exp(-(x-μ)² / (2σ²))
auto gaussian = (1.0 / (make_expr(sigma) * sqrt(constant<double>(2.0 * 3.14159265359)))) *
exp(-(make_expr(x) - make_expr(mu)) * (make_expr(x) - make_expr(mu)) /
(2.0 * make_expr(sigma) * make_expr(sigma)));
double prob = gaussian(x = 1.0, mu = 0.0, sigma = 1.0);
std::cout << "PDF(x) = (1/(σ*sqrt(2π))) * exp(-(x-μ)²/(2σ²))" << std::endl;
std::cout << " x=1.0, μ=0.0, σ=1.0" << std::endl;
std::cout << " 概率密度(probability density) = " << prob << std::endl << std::endl;
// 示例4:展示符号的唯一性
// Example 4: Demonstrate symbol uniqueness
std::cout << "=== 示例4:符号唯一性验证 ===" << std::endl;
std::cout << "=== Example 4: Symbol uniqueness verification ===" << std::endl;
symbol<[] {}, real> s1;
symbol<[] {}, real> s2;
symbol<[] {}, real> s3;
std::cout << "s1和s2是同一类型? "
<< std::is_same_v<decltype(s1), decltype(s2)> << " (应为1/true)" << std::endl;
std::cout << "s1 and s2 same type? "
<< std::is_same_v<decltype(s1), decltype(s2)> << " (should be 1/true)" << std::endl;
std::cout << "s1和s3是同一类型? "
<< std::is_same_v<decltype(s1), decltype(s3)> << " (应为0/false)" << std::endl;
std::cout << "s1 and s3 same type? "
<< std::is_same_v<decltype(s1), decltype(s3)> << " (should be 0/false)" << std::endl;
return 0;
}
1) 高层概述(这段代码做什么)
这是一套表达式模板 + 符号绑定的小框架,允许你以类似数学表达式的写法构造表达式树(a * sin(w * t + phi) 等),然后以语法 f(a = 5.0, w = 2.5, t = 1.6, phi = 0.0) 把具体数值“绑定”到符号上并求值。
主要组件包括:
- 一组类型工具(处理引用/const 重限定等)。
- 一组“约束”模板(
real,integer,unconstrained等)用于限制符号可绑定的类型。 - 使用 lambda trick 为符号生成唯一的类型 ID(
symbol_id/symbol)。 symbol_binder保存“符号 = 值”的绑定。- 表达式模板类型:
constant、symbol_node、binary_op、unary_op。 - 一系列运算符对象(
add_op、sin_op等)与相应的运算符/函数重载,用于构建表达式树。 make_expr将symbol转换为symbol_node从而可参与表达式构造。main()包含 4 个示例(正弦、二次式、高斯 PDF、符号唯一性验证)。
2) 关键类型工具(简述和目的)
文件开头有若干模板用于引用/const 操作:
remove_lvalue_reference<T>:仅去掉左值引用(T&->T),保留右值引用(T&&不变)。用于处理模板参数时保留语义。remove_rvalue_reference<T>:仅去掉右值引用(T&&->T),保留左值引用。requalify_as_const<T>:把类型重新限定为const,但保留引用性质:int->const intint&->const int&int&&->const int&&
这些工具在symbol_binder中用于选择合适的存储类型(尽量避免不必要拷贝与保持正确的引用/const 语义)。
3) 约束(concept 模拟)
unconstrained<T>:总是true_type,不限制类型。real<T>:封装std::is_floating_point<T>,用于限制符号只能绑定浮点类型。integer<T>、arithmetic<T>类似。
这些模板被用于symbol的赋值 operator= 上的requires,确保绑定类型满足约束(例如real只能绑定浮点数)。
4) 符号 ID 与 lambda 技巧(如何得到唯一类型)
核心思想:用一个 lambda 闭包对象作为 non-type 模板参数,从而让每个不同的 lambda 表达式产生独一无二的类型(lambda 的闭包类型是唯一的)。实现细节:
template <auto Lambda> struct symbol_id { static constexpr auto singleton = Lambda; static constexpr const void* address = std::addressof(singleton); };singleton保存 lambda 实例(constexpr),address保存其地址(用作比较)。
- 重载
operator<=>和operator==来允许不同symbol_id之间比较(通过地址比较封装的 lambda 单例地址)。
注意(重要):在 C++ 中每个 lambda 表达式都有其唯一的闭包类型。如果你在不同位置写了两个文本上相同的[] {}表达式,它们通常仍然是不同的闭包类型(因此不是同一个 symbol)。如果想要多个变量共享同一个 symbol 类型,应该把闭包对象作为一个constexpr名字变量(例如static constexpr auto id = []{};),然后在多个地方使用同一个id。我在后面“潜在问题”会详细说明这个点。
5) symbol_binder(符号绑定器)
template <typename Symbol, typename T> struct symbol_binder:
symbol_type:绑定对应的Symbol类型(用于symbol_node在查找绑定时进行类型匹配)。- 保存值的成员
value的类型是requalify_as_const_t<remove_rvalue_reference_t<T>>:- 目的:把要保存的值以合适的形式(带
const)存储,既支持引用语义也避免不必要拷贝。
- 目的:把要保存的值以合适的形式(带
- 构造函数模板
symbol_binder(Symbol, U&& x)使用requires std::is_convertible_v<U, remove_rvalue_reference_t<T>>,并转发x到value,保证可转换并尽量使用noexcept推断。 operator()()返回const value_type&,供symbol_node在求值时调用以得到绑定的值。
另外代码提供了 deduction guidesymbol_binder(Symbol, T&&) -> symbol_binder<Symbol, T>,便于自动推导 binder 的模板参数。
6) symbol(符号类)
template <auto Id = [] {}, template <typename> typename Constraint = unconstrained> struct symbol { ... };
Id:用来唯一标识符号(使用 lambda)。Constraint:用于限制符号可绑定的类型(例如real)。operator=(Arg&& arg) const:当你写a = 5.0时,会创建symbol_binder(*this, std::forward<Arg>(arg))。requires子句使用Constraint<std::remove_cvref_t<Arg>>::value做静态类型检查。
这让写f(a = 5.0, ...)成为可能,a = 5.0是一个 binder 对象。
7) 表达式模板层次(如何表示与求值表达式)
CRTP 基类:
template <typename Derived> struct expression:供所有节点继承,主要用于std::derived_from要求和统一接口。
节点类型:constant<T>:保存一个常数值,operator()(Binders&&...)忽略任何 binder,直接返回常量。symbol_node<Symbol>:在求值时从传入的 binder 列表中逐个递归查找匹配Symbol的symbol_binder,找到后调用first()返回其值。- 注意:实现是递归式的
get_value(First, Rest...);如果没有找到匹配的 binder,会在编译期尝试继续展开直到没有参数 —— 程序中没有提供“未找到时的终止 overload”或明确的静态断言,缺 binder 的情况会导致编译错误(通常为模板匹配失败或找不到get_value()的可调用重载)。
- 注意:实现是递归式的
binary_op<Op, Left, Right>/unary_op<Op, Arg>:分别评估子表达式并将Op{}应用到得到的结果上。
8) 运算符对象与运算符重载
add_op,sub_op,mul_op,div_op,neg_op,sin_op,cos_op,exp_op,sqrt_op等分别封装具体的运行时操作(大多数直接调用标准算术/std::sin等)。- 通过一系列
operator+/operator-/operator*/operator/的重载,支持:- expression + expression
- expression + numeric
- numeric + expression
- 同理对减/乘/除
sin(expr)、cos(expr)、exp(expr)、sqrt(expr)被定义为返回对应的unary_op<..., E>,从而可以直接写sin(make_expr(w) * make_expr(t) + make_expr(phi))。
9) make_expr 与 formula
make_expr(symbol<...> s)—— 把symbol转为symbol_node<symbol<...>>,即把符号纳入表达式系统。formula<Expr>包装表达式树,使其本身是expression派生类型,并提供operator()(Binders&&...)转发到内部的表达式(Expr)上。主程序里并没有显式使用formula(可以用来包裹更复杂的表达式以便重用/传递)。
10) main() 做了哪些具体示例(并给出数值结果)
程序在 main() 中演示了 4 个示例 —— 我把每个示例的意图与精确数值列出(数值是用高精度数学库计算并在回答中贴出):
示例1:正弦波 y(t) = a * sin(w * t + phi)
代码:
symbol<[] {}, real> a, w, t, phi;
auto f = make_expr(a) * sin(make_expr(w) * make_expr(t) + make_expr(phi));
double y = f(a = 5.0, w = 2.5, t = 1.6, phi = 0.0);
计算结果(精确到双精度):
w * t=2.5 * 1.6=4.0sin(4.0)≈-0.7568024953079282y = 5.0 * sin(4.0)≈-3.7840124765396412
(代码会打印类似结果(result) = -3.7840124765396412。)
示例2:二次方程 f(x) = x^2 + b*x + c,评估点 x=2.0,b=3.0,c=1.0
代码:
symbol<[] {}, real> x, b, c;
auto quad = make_expr(x) * make_expr(x) + make_expr(b) * make_expr(x) + make_expr(c);
double result = quad(x = 2.0, b = 3.0, c = 1.0);
计算:
x^2 = 2^2 = 4b*x = 3*2 = 6- 总和
4 + 6 + 1 = 11→11.0
示例3:高斯分布概率密度函数(标准正态在 x=1)
公式:
PDF(x) = (1 / (σ * sqrt(2π))) * exp(-(x-μ)² / (2σ²))
代码里用 mu、sigma、x 构造该表达式并用 x=1.0, mu=0.0, sigma=1.0 评估。
结果(标准正态在 1 的概率密度):
- ≈
0.24197072451914337
示例4:符号唯一性验证(注意:代码作者的注释/预期与实际行为需注意)
代码声明:
symbol<[] {}, real> s1;
symbol<[] {}, real> s2;
symbol<[] {}, real> s3;
std::cout << std::is_same_v<decltype(s1), decltype(s2)> << ...;
- 重要说明(常见误解):每个 lambda 表达式在 C++ 中具有独一无二的闭包类型。即使文本上写两次
[] {},在多数情况下它们也是不同的类型(除非你把同一个闭包对象命名并复用)。因此,如果你在多个独立位置各写一个[] {}并把它们作为模板非类型参数,很可能得到不同的symbol类型。 - 如果你 真的想 让
s1与s2是相同类型(相同 symbol),应该把 lambda 单例放到一个constexpr变量中并复用,例如:static constexpr auto id = []{}; symbol<id, real> s1, s2; // s1, s2 将是相同类型 - 因此,程序中期待的输出注释(
s1和s2是同一类型? ... (应为1/true))可能不成立,取决于编译器对相同文本 lambda 表达式是否被视为同一闭包(通常不是)。更稳妥的方式是显式复用同一个 id。
11) 潜在问题、边界条件与改进建议
- 符号绑定缺失导致的错误信息不友好
symbol_node::get_value递归查找没有为“未找到”情况提供明确静态断言或错误消息。建议添加一个基 case 或在最外层提供static_assert(false, "...")(用 SFINAE/概念技巧以便在编译时给出清晰提示),否则编译器会报非常冗长/难懂的模板错误。
- lambda 用法的细节
- 如上所说,文本上相同的
[]{}并不保证产生相同闭包类型。若希望可重用符号,推荐使用static constexpr auto id = []{};并用symbol<id>。
- 如上所说,文本上相同的
symbol_binder的存储策略- 目前将值存为
requalify_as_const_t<remove_rvalue_reference_t<T>>,这有助于避免意外修改,但可能对大对象的移动/拷贝策略需要更细的控制(例如:希望转移所有权时应支持移动语义并且不总是const)。根据使用场景可权衡。
- 目前将值存为
- 表达式中常量与类型推导一致性
- 你在表达式中既有
constant<double>(2.0 * 3.14159265359),也有裸1.0(当与表达式混合时,运算符重载会把1.0包装成constant<double>)。这没问题,但要注意不同常量类型(int,double)下的隐式转换与精度问题。
- 你在表达式中既有
- 可扩展性/性能
- 表达式模板在编译期会生成大量的小对象/内联函数,若表达式层级很深可能导致编译慢或二进制膨胀。可以考虑对常见子表达式做
constexpr评估或按需缓存(但这会增加复杂度)。
- 表达式模板在编译期会生成大量的小对象/内联函数,若表达式层级很深可能导致编译慢或二进制膨胀。可以考虑对常见子表达式做
12) 示例输出(精确数值汇总)
- 示例1(
y = a * sin(w * t + phi),a=5.0,w=2.5,t=1.6,phi=0.0):y ≈ -3.7840124765396412
- 示例2(二次式,
x=2.0,b=3.0,c=1.0):result = 11.0
- 示例3(标准正态 PDF 在
x=1):prob ≈ 0.24197072451914337
13) 总结(简短)
- 这段代码实现了一个类型安全、可约束的符号表达式系统,利用 lambda 作为符号唯一标识(非类型模板参数),以表达式模板的方式构造和求值数学公式。
- 关键要点:注意 lambda 的唯一性语义(若需要相同 symbol,请复用同一个
constexprlambda 对象),并为“找不到绑定”这种情况添加明确静态检查以提高可用性。 - 如果你想,我可以:
- 把示例 4 的 symbol 唯一性部分改写为更确定的写法(用
static constexpr auto),并展示编译器行为; - 给
symbol_node::get_value增加一个友好的静态断言/错误信息; - 或将
formula包裹使用示例写出来,便于复用表达式。
- 把示例 4 的 symbol 唯一性部分改写为更确定的写法(用
更多推荐


所有评论(0)