第七章:表达式模板的使用

在实现表达式模板之前,我们先做一些准备工作。

首先实现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()]() // C++20
{
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 {}; // 空类,默认占 1 字节

    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; // 推导为 double

解释:因为 int 可以隐式转换成 double,但 double 不能无损转为 int,所以 double 是更“通用”的类型。


✅ 示例 2:三个类型

1
std::common_type_t<int, float, double> x; // 结果是 double

解释:intfloat 都能隐式转换为 double,所以 double 是最合适的公共类型。


✅ 示例 3:自定义类型

1
2
3
4
struct A {};
struct B : A {};

std::common_type_t<A, B> obj; // 推导为 A

解释: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::ValueTypeintR::ValueTypedouble,我们就自动推导出 ValueType = double,避免类型不匹配。


⚠️ 注意事项

  1. std::common_type_t 会考虑 三元运算符规则

    1
    std::common_type_t<int, long, float> // 是 float(因为 int ? long : float 是 float)
  2. 如果两个类型没有公共类型(比如你自定义的类型没有相互转换),则会编译失败。


🛠️ 实际用途

  • 推导表达式模板的值类型(+-* 等)
  • 函数模板返回值统一
  • 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()