第七章:表达式模板的使用 在实现表达式模板之前,我们先做一些准备工作。
首先实现action之间的依赖,一个action可以触发其他的action:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 TEST (ReactionTest, TestAction){ auto a = reaction::var (1 ); auto b = reaction::var (3.14 ); auto at = reaction::action ([](int aa, double bb) { std::cout << "a = " << aa << '\t' << "b = " << bb << '\t' ; }, a, b); bool trigger = false ; auto att = reaction::action ([&]([[maybe_unused]] auto atat) { trigger = true ; std::cout << "at trigger " << std::endl; }, at); trigger = false ; a.value (2 ); EXPECT_TRUE (trigger); }
我们需要对void类型做一个包装:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 struct VoidWrapper { }; template <>class Resource <VoidWrapper> : public ObserverNode{ public : VoidWrapper getValue () const { return VoidWrapper{}; } };
然后实现数据源的重置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 TEST (ReactionTest, TestReset){ auto a = reaction::var (1 ); auto b = reaction::var (2 ); auto ds = reaction::calc ([](auto aa, auto bb) { return aa + bb; }, a, b); auto dds = reaction::calc ([](auto aa, auto bb) { return aa + bb; }, a, b); dds.reset ([](auto aa, auto dsds) { return aa + dsds; }, a, ds); a.value (2 ); EXPECT_EQ (dds.get (), 6 ); }
这里涉及到一个问题,之前我们是用一个tuple来保存形参包的,现在这个形参包的类型和个数会被重置,那么如何保存一个可变的形参包呢? 答案是std::function:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 template <typename Fun, typename ... Args>class Expression : public Resource<ReturnType<Fun, Args...>>{ public : using ValueType = ReturnType<Fun, Args...>; using ExprType = CalcExpr; template <typename F, typename ... A> void setSource (F &&f, A &&...args) { if constexpr (std::convertible_to<ReturnType<std::decay_t <F>, std::decay_t <A>...>, ValueType>) { this ->updateObservers (std::forward<A>(args)...); setFunctor (createFun (std::forward<F>(f), std::forward<A>(args)...)); evaluate (); } } private : template <typename F, typename ... A> auto createFun (F &&f, A &&...args) { return [f = std::forward<F>(f), ...args = args.getPtr ()]() { if constexpr (VoidType<ValueType>) { std::invoke (f, args->get ()...); return VoidWrapper{}; } else { return std::invoke (f, args->get ()...); } }; } void valueChanged () override { evaluate (); this ->notify (); } void evaluate () { if constexpr (VoidType<ValueType>) { std::invoke (m_fun); } else { this ->updateValue (std::invoke (m_fun)); } } void setFunctor (const std::function<ValueType()> &fun) { m_fun = fun; } std::function<ValueType()> m_fun; };
然后实现()语法糖的封装,实现自动注册:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 TEST (ReactionTest, TestParentheses) { auto a = reaction::var (1 ); auto b = reaction::var (3.14 ); EXPECT_EQ (a.get (), 1 ); EXPECT_EQ (b.get (), 3.14 ); auto ds = reaction::calc ([&]() { return a () + b (); }); auto dds = reaction::calc ([&]() { return std::to_string (a ()) + std::to_string (ds ()); }); ASSERT_FLOAT_EQ (ds.get (), 4.14 ); EXPECT_EQ (dds.get (), "14.140000" ); a.value (2 ); ASSERT_FLOAT_EQ (ds.get (), 5.14 ); EXPECT_EQ (dds.get (), "25.140000" ); }
1 2 3 4 5 6 7 8 9 10 template <typename F>void set (F &&f) { g_reg_fun = [this ](NodePtr node) { this ->addObCb (node); }; return this ->setSource (std::forward<F>(f)); g_reg_fun = nullptr ; }
最后是表达式模板的封装:
1 2 3 4 5 6 7 8 9 10 11 12 13 TEST (ReactionTest, TestExpr) { auto a = reaction::var (1 ); auto b = reaction::var (2 ); auto c = reaction::var (3.14 ); auto ds = reaction::calc ([&]() { return a () + b (); }); auto expr_ds = reaction::expr (c + a / b - ds * 2 ); a.value (2 ); EXPECT_EQ (ds.get (), 4 ); ASSERT_FLOAT_EQ (expr_ds.get (), -3.86 ); }
一、表达式模板是什么? 表达式模板(Expression Template) 是一种 延迟求值(Lazy Evaluation) 技术,通过 C++ 模板和运算符重载,在编译期构建表达式的语法树,然后在需要时一次性计算整个表达式,避免中间结果的拷贝或临时对象的生成 。
最早由 Todd Veldhuizen 在处理 C++ 数组库(如 Blitz++)时提出,用于提高性能。
二、它解决了什么问题? 举个例子:
1 2 Vector a, b, c, d; a = b + c + d;
传统实现中,b + c
产生一个临时对象 temp1
,然后 temp1 + d
再产生另一个临时对象 temp2
,最终赋值给 a
,造成了多次内存分配和复制。
而使用表达式模板,b + c + d
不会立即执行,而是生成一个表达式类型 AddExpr<AddExpr<b, c>, d>
,等 a = ...
的时候再统一执行,从而避免了中间变量,提高效率。
三、基本原理 核心思想是:
重载运算符返回表达式类型 (如 +
返回 BinaryOpExpr
)
延迟求值 ,即表达式本身只是“记录”结构
调用时再执行 (如 operator()
或强制类型转换)
示例结构: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 template <typename T>struct Value { using ValueType = T; Value (T val) : value (val) {} T operator () () const { return value; } operator T () const { return value; } private : T value; }; template <typename Op, typename L, typename R>struct BinaryOpExpr { using ValueType = std::common_type_t <typename L::ValueType, typename R::ValueType>; BinaryOpExpr (L l, R r, Op o = Op{}) : left (l), right (r), op (o) {} ValueType operator () () const { return op (left (), right ()); } private : L left; R right; Op op; }; template <typename L, typename R>auto operator +(L l, R r) { return BinaryOpExpr <AddOp, L, R>(l, r); }
四、表达式模板的优点 ✅ 性能优化(零拷贝) ✅ 编译期构建计算图 ✅ 高度可组合 ✅ 适用于数学类库(矩阵、向量)、响应式编程、DSL 构建等
五、表达式模板的应用场景
场景
举例
数值计算
Blitz++、Eigen、xtensor
图形编程
OpenGL DSL、Shader 表达式
信号处理
响应式框架:RxCpp、C++ reactive dataflow
自定义 DSL
嵌入式编程、财务表达式等
表达式构建器
SQL 查询构建、规则引擎
https://godbolt.org/z/z4Ye5PqsK
在 C++ 中,空类(没有任何成员变量或非静态成员函数的类)默认情况下会占用 1 字节 的内存空间(用于确保不同对象的地址唯一)。但可以通过以下方法去掉或优化其大小:
使用 [[no_unique_address]]
(C++20 引入)
作用 :允许空成员变量不占用额外空间(如果它是空的)。
适用场景 :空类作为成员变量时(如空基类、无状态的策略类)。
示例 :1 2 3 4 5 6 7 8 struct Empty {}; struct Optimized { [[no_unique_address]] Empty e; int x; }; static_assert (sizeof (Optimized) == sizeof (int ));
注意 :是否真正优化取决于编译器实现和成员布局规则(如 MSVC 可能仍需对齐)。
std::common_type_t
是 C++11 引入的一个类型萃取工具,用于从多个类型中推导出它们的公共类型 ,即它们能隐式转换到的最“宽泛”的一个类型。
🧠 基本概念 1 2 template <class ... T>using common_type_t = typename std::common_type<T...>::type;
它是 std::common_type<T...>
的类型别名(简写形式)。
用于推导多个类型的“共同类型 ”(比如用于运算、返回值、参数统一等场景)。
✅ 示例 1:两个基本类型 1 std::common_type_t <int , double > x;
解释:因为 int
可以隐式转换成 double
,但 double
不能无损转为 int
,所以 double
是更“通用”的类型。
✅ 示例 2:三个类型 1 std::common_type_t <int , float , double > x;
解释:int
、float
都能隐式转换为 double
,所以 double
是最合适的公共类型。
✅ 示例 3:自定义类型 1 2 3 4 struct A {};struct B : A {};std::common_type_t <A, B> obj;
解释:B
可以转为 A
,所以 A
是公共基类型。
✅ 示例 4:结合表达式模板 在表达式模板中,我们通常这样写:
1 2 3 4 5 template <typename Op, typename L, typename R>class BinaryOpExpr {public : using ValueType = std::common_type_t <typename L::ValueType, typename R::ValueType>; };
**目的:**如果 L::ValueType
是 int
,R::ValueType
是 double
,我们就自动推导出 ValueType = double
,避免类型不匹配。
⚠️ 注意事项
std::common_type_t
会考虑 三元运算符规则 :
1 std::common_type_t <int , long , float >
如果两个类型没有公共类型(比如你自定义的类型没有相互转换),则会编译失败。
🛠️ 实际用途
推导表达式模板的值类型(+
、-
、*
等)
函数模板返回值统一
SFINAE 判断(如果不存在共同类型就不启用函数)
类型安全统一转换
🧾 总结表
示例
std::common_type_t<T1, T2>
的结果
int, double
double
float, double
double
int, unsigned int
unsigned int
A, B
(B继承自A)
A
std::string, const char*
std::string
(因为 const char* 可转为 string)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 BasedOnStyle: LLVM IndentWidth: 4 ColumnLimit: 0 BinPackArguments: true AccessModifierOffset: -4 AlignAfterOpenBracket: DontAlign AllowShortIfStatementsOnASingleLine: true option (ENABLE_CLANG_TIDY "Enable clang-tidy analysis" OFF) if (ENABLE_CLANG_TIDY) set (CMAKE_CXX_CLANG_TIDY clang-tidy-19 ; -quiet; -checks=modernize-*,readability-*,performance-*,-readability-identifier-length,-readability-magic-numbers,-modernize-use-trailing-return -type,-readability-function-cognitive-complexity ) endif ()