内容探讨了 C++ 中的继承方式,并提到了 Mixins 和 CRTP(Curiously Recurring Template Pattern),还引用了著名的一句话:
“Inheritance is the base class of evil”
——Sean Parent
下面是对这些概念的中文解释和理解:
继承(Inheritance)
在 C++ 中,继承是面向对象编程的核心特性之一,但随着语言的发展和需求的复杂化,继承模式的使用也变得更复杂,甚至可能带来维护和扩展性的问题。
C++ 支持的继承类型
- 单继承(Single Inheritance)
一个类只继承一个父类:class Base {}; class Derived : public Base {};
- 多继承(Multiple Inheritance)
一个类可以继承多个父类:class A {}; class B {}; class C : public A, public B {};
- 变参继承(Variadic Inheritance)
使用模板参数包实现多个父类继承(C++11 起):template<typename... Bases> struct Mixin : public Bases... {};
为什么说 “继承是万恶之源”?
Sean Parent 的这句话在 C++ 社区被广泛引用,核心意思是:
- 继承容易导致强耦合。
- 子类可能依赖父类的实现细节,导致修改难度大。
- 多继承容易产生歧义(如菱形继承问题)。
- 虚函数带来运行时开销。
- 一些继承关系更适合用组合(composition)来表达。
Mixins 是什么?
Mixins 是一种编程技巧,目的是“组合功能”而不是“扩展类型”。C++ 中没有关键字 mixin
,但可以通过 模板继承 来模拟 Mixins。
一个简单的 C++ Mixin 示例:
template<typename T>
struct MixinLogger {void log() const { std::cout << "Logging..." << std::endl; }
};
class MyComponent : public MixinLogger<MyComponent> {// 增加了 log() 能力
};
Mixins 本质上是一种 通过继承添加行为 的方法,和 CRTP(下文讲)经常一起使用。
CRTP(Curiously Recurring Template Pattern)
CRTP 是实现静态多态和 mixin 的核心工具:
template<typename Derived>
class Base {
public:void interface() {static_cast<Derived*>(this)->implementation();}
};
class Derived : public Base<Derived> {
public:void implementation() {std::cout << "Derived implementation" << std::endl;}
};
它的特点是:
- 基类使用派生类作为模板参数。
- 编译器可以展开函数调用链而无需虚函数,性能更好。
- 常用于静态接口、策略模式、mixins、编译时检查。
小结
概念 | 说明 |
---|---|
继承 | 强化 is-a 关系,有时会带来复杂度 |
Mixins | 组合多个功能类,提升代码复用 |
CRTP | 模板静态继承技巧,可用于实现 mixins 或接口 |
Sean Parent 的观点 | 提倡组合优于继承,更推荐使用 mixin、策略模式等 |
实践建议
- 尽量使用组合(has-a)而不是继承(is-a)建模行为。
- 如果需要通过继承添加功能,使用 CRTP + mixins 更灵活、安全、性能好。
- 避免深层继承体系,尤其是多个虚函数的多继承设计。
- 如果你对 mixin 模式感兴趣,可以参考如 Boost.Mixin、
sqlpp11
等库。
C++ 中“组合(Composition)”相对于“继承(Inheritance)”的局限性。我们来逐条用中文解释这些问题的意思,并看看有没有对策或理解上的提升。
Composition 的限制 — 中文理解
组合:指的是“has-a”关系,比如一个类中有其他类作为成员变量。
你提到的三点限制如下:
1. “我必须提前知道要组合多少个成员”
解释:
在 C++ 中,如果你使用组合,你通常需要在类定义时就写清楚有哪些成员变量。例如:
struct Logger {};
struct Serializer {};
class MyClass {Logger logger;Serializer serializer;
};
这意味着,你不能在编译期“动态”组合任意数量的模块。
为什么继承更灵活?
使用继承 + 模板时你可以这样做:
template<typename... Mixins>
class MyObject : public Mixins... {};
你可以用任意数量的 Mixins 来组合行为 —— 这就是**变参继承(variadic inheritance)**的优势。
2. “我必须预先决定成员的名字,不能在编译期决定名称”
解释:
在组合中,成员变量必须有确定的名字:
class MyClass {Logger logger;Serializer serializer;
};
你不能根据模板参数,动态生成 logger
、serializer
等成员的名字。
这与例如 Python 的 setattr(obj, name, value)
很不同。
3. “如果我想让类暴露这些成员的方法,我必须手动写转发函数”
解释:
在组合模式下,如果你希望用户通过 myObject.serialize()
调用 serializer.serialize()
,你必须手动写一个函数转发:
class MyClass {Serializer serializer;
public:void serialize() {serializer.serialize();}
};
相比之下,继承就可以直接把基类的方法“暴露”到派生类中:
class MyObject : public Serializer {// 直接就有 serialize() 方法了
};
所以说:
- 组合虽然在 设计上更健壮(解耦),但在 表达能力、编程便利性 上不如继承。
- 这也是为什么 C++ 高级开发者会使用 CRTP、Mixins、变参继承 来模拟“组合的灵活性 + 继承的便利性”。
一种现代方案:用模板 + 继承模拟“可组合模块”
template<typename... Modules>
struct Component : public Modules... {using Modules::operator()...; // 暴露成员函数(C++20 起)
};
struct Log {void log() const { std::cout << "log()" << std::endl; }
};
struct Serialize {void serialize() const { std::cout << "serialize()" << std::endl; }
};
int main() {Component<Log, Serialize> c;c.log();c.serialize();
}
这样你可以像组合一样灵活,同时拥有继承那种方法暴露 + 多模块能力聚合的优势。
总结
问题 | 组合的限制 | 解决方式 |
---|---|---|
数量不定 | 组合不能变参 | 用 变参模板继承 |
名字固定 | 成员名必须提前写死 | 无法完美解决,可通过 tuple + get<N> 绕过 |
方法转发 | 需要手动转发成员方法 | C++20 的 using ...::operator()... 可改善 |
这个例子,来自于 sqlpp11 这个库,它运用了 Variadic CRTP(可变参数的 CRTP 模板)来实现强类型的 SQL 查询结果映射,非常巧妙。
for (const auto& row : db(select(p.id, p.name).from(p).where(p.id > 42))) {int64_t id = row.id;std::string name = row.name;
}
Variadic CRTP 背景理解
- CRTP:模板类以派生类作为模板参数,静态多态。
- Variadic Templates:可变参数模板,允许接收不定数量的模板参数。
- 结合两者,可以实现对任意数量成员的静态处理和组合。
sqlpp11 里的用法示意(简化)
template<typename... Columns>
struct Row : public Columns... {// 每个 Column 都是一个字段封装类,拥有对应的成员名和类型
};
// 查询返回 Row 的一个容器,比如 std::vector<Row<p.id, p.name>>
auto result = db(select(p.id, p.name).from(p).where(p.id > 42));
for (const auto& row : result) {int64_t id = row.id; // 访问 row 中 id 字段std::string name = row.name; // 访问 row 中 name 字段
}
关键点解释:
Row
继承了所有字段类型(Columns...
),每个字段对应一个成员变量和成员函数。- 由于是模板参数包,
Row
可以动态组合任意数量的字段。 - 每个字段都带有名字(成员变量名),因此你可以直接用
.id
、.name
访问。 - 这种设计的背后,通常用到了 CRTP 和模板元编程,自动生成成员变量和访问接口。
为什么 Variadic CRTP 很强大?
- 灵活组合字段,不用写重复代码。
- 编译时类型安全,确保访问字段时类型正确。
- 接口清晰,结果行就像普通结构体一样访问字段。
- 性能好,避免运行时反射开销。
段代码展示了一个很典型的 Variadic CRTP + 多重继承 的高级用法,尤其是在像 sqlpp11 这样的库里用来实现“策略组合”和“功能扩展”的模式。
我帮你分解并解释这段代码:
代码结构回顾
template<typename Db,typename... Policies>
struct statement_t :public expression_operators<statement_t<Db, Policies...>,value_type_of<detail::statement_policies_t<Db, Policies...>>>,public detail::statement_policies_t<Db, Policies...>::_result_methods_t,public Policies::template _member_t<detail::statement_policies_t<Db, Policies...>>...,public Policies::template _methods_t<detail::statement_policies_t<Db, Policies...>>...{};
逐行解析
- 模板参数
Db
:数据库类型(可能封装了数据库连接等)Policies...
:一组“策略”类型,可能是查询策略、过滤策略、排序策略等,用可变参数传入
- 继承表达式操作符
public expression_operators<statement_t<Db, Policies...>,value_type_of<detail::statement_policies_t<Db, Policies...>>
>
expression_operators
可能是一个模板类,提供重载的操作符(比如==
,<
,+
等)使得statement_t
支持表达式式调用。- 传入的第一个参数是 CRTP:自己类型
statement_t<Db, Policies...>
。 - 第二个参数是返回值类型(可能是查询结果的类型),通过
value_type_of<...>
计算。
- 继承一个叫
_result_methods_t
的类型
public detail::statement_policies_t<Db, Policies...>::_result_methods_t,
statement_policies_t<Db, Policies...>
是一个组合了所有策略的辅助类型(策略聚合器)。- 这个聚合体中定义了
_result_methods_t
,可能包含和结果相关的接口,比如.execute()
,.fetch()
等。 statement_t
继承它获得“结果操作”的功能。
- 继承多个
_member_t
模板实例
public Policies::template _member_t<detail::statement_policies_t<Db, Policies...>
>...,
- 对于每个策略
Policies
,它们都有一个成员模板_member_t
。 - 这个成员模板被实例化为
Policies::_member_t<detail::statement_policies_t<Db, Policies...>>
,也就是针对组合的策略类型的成员实现。 - 这让每个策略都能往
statement_t
注入自己的成员变量或状态。
- 继承多个
_methods_t
模板实例
public Policies::template _methods_t<detail::statement_policies_t<Db, Policies...>
>...
- 同样,每个策略还有一个
_methods_t
模板,注入方法(函数)。 - 这些方法让
statement_t
能提供策略定义的各种接口,比如.where()
,.order_by()
等。
设计总结
- Variadic CRTP + 多继承,将多个策略的成员变量和方法“拼装”到一个类型中。
- 策略聚合体 (
statement_policies_t
) 作为中枢协调整体状态和类型。 - 通过模板元编程,
statement_t
变成了一个功能非常灵活且强类型的查询对象。 - 每个策略负责自己的一块责任,互不干扰,易于扩展。
直观理解
你可以把它想成:
struct statement_t : expression_operators<...>,result_methods,policy1_member,policy2_member,policy1_methods,policy2_methods
{};
而这个结构的成员和方法都由策略(Policies)模板参数“按需注入”,实现了高度灵活的组合。
这段代码展示了一个 Variadic CRTP 在 SQL 查询构建中的应用示例,具体是构造一个“空白的”select语句,然后通过columns(...)
方法动态添加列。
代码分解
1. 定义了一个别名模板 blank_select_t
template<typename Database>
using blank_select_t = statement_t<Database,select_t,no_select_flag_list_t,no_select_column_list_t,no_from_t,no_where_t,no_group_by_t,no_having_t,no_order_by_t,no_limit_t,no_offset_t>;
blank_select_t
是statement_t
的一个特化版本,用于表示一个空白的select语句。- 传入了各种策略类型,基本都是“空”策略(例如
no_where_t
代表没有where子句)。 - 这个类型可以理解为一个基础的select语句框架。
2. select
函数模板
template<typename... Columns>
auto select(Columns... columns) -> decltype(blank_select_t<void>().columns(columns...))
{return blank_select_t<void>().columns(columns...);
}
- 接收任意数量的列参数(
Columns...
)。 blank_select_t<void>()
创建了一个空白select对象(此处用void
占位Database类型,实际库里是具体数据库类型)。- 通过调用
.columns(columns...)
方法,将传入的列动态加入到select查询里。 - 利用
decltype
推导返回类型,保持类型安全。
设计理念总结
- Variadic 模板让你可以传入任意数量的列。
- 先通过
blank_select_t
构建一个“空的”select对象。 - 再调用成员函数
columns(...)
向select对象添加列,返回一个新的包含这些列的查询对象。 - 这种设计使得SQL构造变得类型安全且灵活。
作用
这样定义后,你就可以写类似:
auto q = select(p.id, p.name, p.age);
编译器会推导出对应包含3列的查询对象类型。
这段代码演示了 Variadic CRTP 设计中的一个典型策略(policy)模板——no_where_t
,它为 SQL 语句提供了一个“无where子句”的默认实现,并且通过 _methods_t
模板为语句类型注入了 where(...)
方法,支持后续链式添加 where 条件。
详细解析
struct no_where_t
{// 额外方法注入点template<typename Policies>struct _methods_t{template<typename... Args>auto where(Args... args) const-> new_statement<Policies, no_where_t, where_t<void, Args...>>{return {static_cast<const derived_statement_t<Policies>&>(*this),where_data_t<void, Args...>{args...}};}};
};
组成部分
1. no_where_t
- 代表“没有 where 子句”的策略类型。
- 主要用于表示当前的查询对象没有
where
限制。
2. 嵌套模板 _methods_t
- 这是一个模板结构体,用来为包含
no_where_t
策略的statement_t
注入成员函数。 Policies
是整个查询的策略组合类型,方便访问完整的上下文。
3. where
方法模板
- 接收任意参数包
Args... args
,表示用户传入的where
条件。 - 返回一个新的
statement
对象,类型是new_statement<Policies, no_where_t, where_t<void, Args...>>
:- 这里调用了
new_statement
模板,表示基于已有策略Policies
,替换no_where_t
为带参数的where_t
策略,代表添加了一个where
条件的新查询对象。
- 这里调用了
return
语句构造新对象:- 用当前对象转型得到的引用
static_cast<const derived_statement_t<Policies>&>(*this)
(CRTP的常用写法,安全转型到派生类型)。 where_data_t<void, Args...>{args...}
是where
条件的数据包装。
- 用当前对象转型得到的引用
设计理念
- 无where策略
no_where_t
提供一个默认的、空的where行为。 - 通过
_methods_t
注入where()
函数,允许用户调用.where(...)
,创建新的带where条件的查询对象(immutable风格,返回新对象)。 - 体现了策略模式 + CRTP + 模板元编程的组合设计。
- 灵活支持链式调用、类型安全的 SQL 查询构造。
用法示例(伪代码)
auto q1 = blank_select_t<void>(); // no where
auto q2 = q1.where(p.id > 42); // 生成带where条件的新语句
这段代码是在用一种更简洁、更“未来感”的写法表达之前的no_where_t
,它用了一种假想的mixin
语法,意图是让“混入”的方法块更清晰、更独立。核心意思跟之前的是一样的。
代码说明
struct no_where_t
{// 语法上假设有个 mixin 关键字,用来定义“混入的成员方法块”mixin _methods_t{template<typename... Args>auto where(Args... args) const-> _new_statement_t<no_where_t, where_t<void, Args...>>{return { *this,where_data_t<void, Args...>{args...} };}};
};
关键点
- mixin _methods_t 是这段代码的核心,代表“混入”的一块方法定义,理论上相当于之前写的:
template<typename Policies>
struct _methods_t
{template<typename... Args>auto where(Args... args) const -> new_statement<Policies, no_where_t, where_t<void, Args...>>{return { static_cast<const derived_statement_t<Policies>&>(*this),where_data_t<void, Args...>{args...} };}
};
- Idea: Copy and paste the mixins body 表示把这个 mixin 里的方法体,复制粘贴到实际要用的类里,达到“代码重用”的目的。
设计意图
- 让
_methods_t
作为“mixin块”显得更独立、易维护。 - 减少模板参数、类型转换,简化写法。
- 期待语言本身能支持更优雅的“mixin”特性(当前C++没有
mixin
关键字,这里是设想)。
总结
这段“假想代码”想表达:
“我们可以把一组方法写成一个mixin块,然后直接复制粘贴到需要它的类里,而不是用繁琐的模板嵌套结构。”
这样做的优点是代码更直观,缺点是实际C++还不支持,需要用模板技巧实现类似效果。
这段代码展示了一个基于 Variadic CRTP 和策略(Policies)设计的 statement_t
模板,核心思想是利用模板参数包展开,把多个策略类的成员和方法“混入”到最终的 statement_t
类型中,实现灵活的组合与扩展。
详细解析
template<typename Db,typename... Policies>
struct statement_t :public Policies::template _member_t<detail::statement_policies_t<Db, Policies...>>... // 多继承,展开所有策略的成员
{using _value_type = value_type_of<Policies...>; // 根据所有策略推导出value_typeusing mixin expression_operators<_value_type>; // 混入表达式操作符using mixin result_provider<Policies>::_result_methods; // 混入每个策略的结果方法using mixin Policies::_methods...; // 展开所有策略的_methods,混入方法集
};
关键点解释
1. 多继承展开策略成员
public Policies::template _member_t<detail::statement_policies_t<Db, Policies...>>...
- 这句利用模板参数包展开,将所有
Policies
中定义的_member_t
模板基类继承进来。 detail::statement_policies_t<Db, Policies...>
是一个辅助类型,通常是对策略参数的封装和聚合,方便传递上下文。- 每个策略都定义它自己的
_member_t
,用来提供数据成员和成员函数。
2. 定义 _value_type
using _value_type = value_type_of<Policies...>;
- 根据所有策略类型,推导出一个
_value_type
,代表当前语句的返回值类型或操作的结果类型。
3. 使用“mixin”机制混入操作符和方法
using mixin expression_operators<_value_type>;
using mixin result_provider<Policies>::_result_methods;
using mixin Policies::_methods...;
expression_operators<_value_type>
:混入针对结果类型定义的一些操作符,比如重载+
,-
,*
,方便表达式链式调用。result_provider<Policies>::_result_methods
:为每个策略混入结果相关的方法。Policies::_methods...
:对每个策略都展开它们的_methods
成员,这些通常是为语句提供的链式接口方法,比如where()
,order_by()
等。
设计理念
- 策略模式:将功能拆分为策略,每个策略提供成员和方法。
- Variadic CRTP:模板参数包实现任意多策略的组合。
- mixin语义:通过
using mixin
(假想写法)将多个策略的方法和成员“混入”一个类中。 - 灵活、扩展性强:用户只需定义自己的策略,组合进
statement_t
就能获得对应功能。
实际C++实现提示
- C++没有
mixin
关键字,这里using mixin
是一种假设写法。 - 实际可用的技巧是多重继承+模板继承+
using
声明把基类方法带进派生类。 - 例如,
using Policies::_methods::where;
或写个模板辅助宏来实现。
提到的“使用名字作为模板参数”,其实是个C++模板语言的“理想愿景”,现在的C++标准还不支持“模板参数是成员名或标识符”,只能用类型、非类型参数(整数、指针、枚举等)。
你给的例子回顾
template<typename T, name x>
struct field
{using type = T;using name = x;using another_name = foo;T x; // 期望这里成员名是模板参数x
};
using my_field = field<int, bar>;
这里的关键点:
name x
想表示“成员变量的名字是模板参数x
”。T x;
希望根据模板参数给成员变量动态命名。
C++目前的限制
- C++模板参数 不能是标识符(名字),只能是类型或者值(整型常量、指针等)。
- 成员变量名 必须在编译时写死,不能用模板参数动态生成名字。
实际可行的替代方案
1. 使用字符串作为名字的替代
虽然不能用“名字”做模板参数,但可以用const char*
或者std::string_view
传递名字做标签:
template<typename T, const char* Name>
struct field {T value;static constexpr const char* name = Name;
};
// 需要定义一个全局constexpr字符串
constexpr char bar_str[] = "bar";
using my_field = field<int, bar_str>;
缺点是不能作为成员变量名,访问只能通过value
,名字只能做标识和元信息。
2. 使用宏或代码生成
利用宏生成带有指定名字的成员变量:
#define DEFINE_FIELD(type, name) \
struct name##_field { \type name; \
};
DEFINE_FIELD(int, bar);
bar_field my_field;
my_field.bar = 42;
这不是模板,而是预处理器技巧。
3. 使用元编程库
像Boost.Hana
或者sqlpp11
等库,会把字段名和字段类型“绑定”成类型标签,再用访问接口绕过直接成员访问:
struct bar_t {};
template<typename T, typename Name>
struct field {T value;
};
using my_field = field<int, bar_t>;
// 访问要用 get<bar_t>(obj) 之类的辅助函数
总结
- C++标准暂不支持“名字作为模板参数”,即不能用模板参数来动态命名成员变量。
- 只能通过标签类型或字符串常量作为“名字”的替代。
- 真正想做到按名字访问,通常要用元编程、映射表或宏辅助。
这个例子,是想用**变参模板展开(variadic template expansion)**来同时声明多个成员变量和继承多个混入(mixin)的方法。
重点解析
template<typename Db,typename... Policies>
struct statement_t
{using _value_type = value_type_of<Policies...>;// 继承多个mixin,展开多个Policies::_methods...using mixin expression_operators<_value_type>;using mixin result_provider<Policies>::_result_methods;using mixin Policies::_methods...;// 数据成员部分想展开所有Policies的成员policies::member_type policies::member_name...;
};
你写的关键是:
- 继承多个Policy的_mmethods,这是典型的变参模板展开,等同于
using mixin Policies1::_methods; using mixin Policies2::_methods; // ...
- 数据成员也想用变参展开,即
Policies1::member_type Policies1::member_name; Policies2::member_type Policies2::member_name; // ...
具体做法示例
假设每个Policy定义了:
struct Policy1 {using _member_t = int;static constexpr const char* member_name = "m1";struct _methods {void foo() { /*...*/ }};
};
struct Policy2 {using _member_t = double;static constexpr const char* member_name = "m2";struct _methods {void bar() { /*...*/ }};
};
成员展开的难点
C++不能直接用模板参数中的名字来定义成员变量名,不能写:
Policy::member_type Policy::member_name; // 这不是合法语法
因为member_name
是字符串常量,不能用作标识符。
解决方案
通常用成员类型和成员变量名封装成一个嵌套结构,然后继承或聚合:
template<typename Policy>
struct member_wrapper {typename Policy::_member_t member;
};
template<typename Db, typename... Policies>
struct statement_t : member_wrapper<Policies>...
{using _value_type = value_type_of<Policies...>;using mixin expression_operators<_value_type>;using mixin result_provider<Policies>::_result_methods;using mixin Policies::_methods...;
};
这样,statement_t
就有了多个来自不同Policy
的成员变量,但成员名都叫member
,放在不同的基类里。
访问成员
访问时,需要通过基类强制转换来访问对应Policy的成员:
statement_t<Db, Policy1, Policy2> stmt;
stmt.member_wrapper<Policy1>::member = 42;
stmt.member_wrapper<Policy2>::member = 3.14;
总结
- C++不能用模板参数中“名字字符串”来命名成员变量。
- 可以用多重继承,每个Policy封装一个成员变量,成员名都一样,但属于不同基类。
- 通过基类限定符访问不同成员。
- 对于方法,变参展开继承mixin类非常直接,写
using mixin Policies::_methods...
即可。
sqlpp11 这种用变参模板实现的“结构体”或者“行”对象,能够让你像访问普通结构体成员一样访问row.id
、row.name
,但实际上成员是通过模板元编程动态生成的。
关键点:
- 变参模板定义一组字段(field),每个字段有名字和类型。
- 通过模板“打包”这些字段,生成一个拥有这些字段成员的类型(类似结构体)。
- 访问时用
.
访问,像普通结构体成员一样。 - 编译时确定字段数量和名称,支持灵活扩展。
简单示例思路
// 字段模板,带名字和类型
template<typename T, const char* Name>
struct field {T value;T& get() { return value; }const T& get() const { return value; }
};
// 由于不能直接用字符串字面量作为模板参数,实际会用tag类型来代替名字:
struct id_tag {};
struct name_tag {};
template<typename T, typename Tag>
struct field2 {T value;T& get() { return value; }const T& get() const { return value; }
};
// named_tuple继承多个field
template<typename... Fields>
struct named_tuple : Fields... {// 访问成员的辅助模板方法template<typename Tag>auto& get() {return static_cast<field2<typename Fields::type, Tag>&>(*this).value;}
};
// 使用示例
using row_t = named_tuple<field2<int64_t, id_tag>,field2<std::string, name_tag>
>;row_t row;
row.get<id_tag>() = 42;
row.get<name_tag>() = "Alice";
访问时:
int64_t id = row.get<id_tag>();
std::string name = row.get<name_tag>();
sqlpp11更高级
- 他们用表达式模板封装字段
- 用CRTP和变参模板组合
- 支持
.id
、.name
直接访问成员(通过模板技巧实现) - 结合数据库查询,返回这些“named tuple”类型的行对象
总结
- 变参模板结合多继承可以动态生成拥有任意字段的类型
- 访问字段通常用tag类型索引或类似技巧
- 能实现类似
row.id
这样的成员访问是因为编译时模板展开和代码生成 - sqlpp11就是这种高阶用法的实际案例,特别适合生成SQL查询结果行的“结构体”
代码片段是个复杂条件判断的lambda,用来匹配cand
和product
的多个字段:
auto automatch = [product](const auto& cand) {return true&& (cand.first.currencyId == product.currencyId || cand.first.currencyId == 0)&& (cand.first.paymentMethodId == product.paymentMethodId || cand.first.paymentMethodId == 0)&& (cand.first.subPaymentMethodId == product.subPaymentMethodId || cand.first.subPaymentMethodId == 0)&& (cand.first.merchantHistoryId == product.merchantHistoryId || cand.first.merchantHistoryId == 0)&& (cand.first.industryId == product.industryId || cand.first.industryId == 0)&& (cand.first.countryId == product.countryId || cand.first.countryId == 0);
};
你想表达的理解点:
- 这是一个典型的“多字段匹配条件”,判断
cand.first
里的多个ID字段是否要么与product
对应字段相等,要么为0(零值表示通配或无要求) - 这是“短路且”逻辑,每个字段的判断都必须为真,整个才匹配成功
- 这种写法很常见于业务逻辑中“多条件过滤”,但代码写起来挺冗长且容易出错
变参成员(Variadic Members)和这段代码的关系?
你的问题是不是想表达:
这种“字段判断逻辑”如果能用“变参模板”自动展开字段比较,会不会写起来更优雅,减少冗余?
这是个常见需求,尤其当你有“多个字段要做相似判断”时。
变参展开写法思路示范
假设你有一组字段,通过模板展开,自动写类似的“匹配逻辑”:
template<typename Product, typename Candidate, typename... Fields>
bool automatch(const Product& product, const Candidate& cand, Fields... fields) {return ( ... && ((getField(cand.first, fields) == getField(product, fields)) || getField(cand.first, fields) == 0));
}
其中getField
是一个辅助函数,用来从结构体中提取对应字段的值。
这样,你只需写一次模板代码,传入字段列表,就能自动生成多个字段的匹配判断,避免手写重复。
总结
- 你给的lambda是手写的多字段匹配条件
- 这种代码在“变参成员”或“模板元编程”框架下可以用模板展开来自动生成,减少冗余
- 这也是
sqlpp11
或类似库用变参CRTP管理多成员时,经常解决的问题
这个思路是用模板函数 andequalOrZero<T>
来封装每个字段的判断,代码更简洁清晰。
比如:
template<typename Field, typename T>
bool andequalOrZero(const T& lhs, const T& rhs) {return (lhs.template get<Field>() == rhs.template get<Field>()) || (lhs.template get<Field>() == 0);
}
或者如果你用结构体成员名直接访问:
template<typename Field, typename T>
bool andequalOrZero(const T& lhs, const T& rhs) {return (lhs.*Field == rhs.*Field) || (lhs.*Field == 0);
}
这样写的优点是:
- 代码复用,避免重复写相同的逻辑
- 更易维护,改规则只需改
andequalOrZero
函数 - 结合模板参数,可以写成可变参数模板,自动展开所有字段判断
甚至可以写成:
template<typename T, typename... Fields>
bool automatch(const T& cand, const T& product) {return ( ... && andequalOrZero<Fields>(cand, product) );
}
然后调用时:
auto result = automatch<currency,paymentMethodId,subPaymentMethodId,merchantHistoryId,industryId,countryId
>(cand.first, product);
这样,就实现了“变参成员”的自动匹配逻辑,符合你之前提到的 Variadic Members
的思想。
总结一下,你的改写方向:
- 以模板封装通用判断逻辑
- 利用模板参数(字段名或成员指针)自动展开判断
- 写出更简洁、通用、可维护的代码
总结
- Mixin:本质上是“代码片段”拷贝到目标位置,模板参数先被应用,名字冲突就像代码直接写在那样处理。
- Name Literals:是一种能够作为名字使用的特殊字面量,能当作模板参数,还能转成字符串字面量(反过来不行)。
这两个概念结合起来,可以让 C++ 写出更灵活、可复用、且更接近“元编程”的代码结构,尤其是在实现复杂 DSL(比如 sqlpp11)时非常有用。
基于 sqlpp11 的变长模板(Variadic CRTP)、Mixin、以及“名字字面量”与表达式组合的复杂示例。要写成“完整代码”非常庞大,涉及很多辅助模板和元编程结构。
整理一个简化版框架代码,模拟它们的核心思想,能体现你提到的:
statement_t
组合多个Policy混入(Mixin)no_where_t
模块化的where()
函数- 支持
select
返回支持成员访问的行类型 - 以及自动匹配条件用函数简化写法
完整简化示例代码
#include <iostream>
#include <string>
#include <vector>
#include <tuple>
// ---------------------
// name literal 模拟
template<char... Cs>
struct name_literal {static constexpr char value[sizeof...(Cs) + 1] = { Cs..., 0 };static constexpr const char* c_str() { return value; }
};
template<char... Cs>
constexpr char name_literal<Cs...>::value[sizeof...(Cs) + 1];
// ---------------------
// Field 模板,带名字字面量和类型
template<typename T, typename Name>
struct field {T value;T& operator()() { return value; }const T& operator()() const { return value; }
};
// ---------------------
// 模拟查询结果行,继承多个 field (variadic members)
template<typename... Fields>
struct row_t : Fields... {// 访问成员的简写函数template<typename Name>auto& get() {return static_cast<field<typename Fields::type, Name>&>(*this).value;}
};
// ---------------------
// 简单Policy示例:select列定义
struct select_t {template<typename Policies>struct _member_t {// select_t 结果示例, 定义id和name字段using type = row_t<field<int64_t, name_literal<'i','d'>>,field<std::string, name_literal<'n','a','m','e'>>>;};template<typename Policies>struct _methods_t {// 可以写select特有方法};
};
// ---------------------
// no_where_t 示例
struct no_where_t {template<typename Policies>struct _methods_t {template<typename... Args>auto where(Args... args) const {// 返回新状态的新statement,省略实现细节std::cout << "where clause called\n";return *this; // 简化示例,实际返回新类型}};
};
// ---------------------
// statement_t核心,混入多个Policy的成员和方法
template<typename Db, typename... Policies>
struct statement_t : Policies::template _member_t<void>::type, // 成员Policies::template _methods_t<void>... { // 方法void print() {std::cout << "statement_t with policies\n";}
};
// ---------------------
// blank_select_t 默认状态
template<typename Db>
using blank_select_t = statement_t<Db, select_t, no_where_t>;
// ---------------------
// select函数,开始构造语句
template<typename... Columns>
auto select(Columns... columns) {return blank_select_t<void>();
}
// ---------------------
// 测试代码
int main() {auto stmt = select("id", "name");stmt.where("id > 42");// 模拟遍历查询结果row_t<field<int64_t, name_literal<'i','d'>>,field<std::string, name_literal<'n','a','m','e'>>> row;row.field<int64_t, name_literal<'i','d'>>::value = 123;row.field<std::string, name_literal<'n','a','m','e'>>::value = "Alice";std::cout << "id = " << row.field<int64_t, name_literal<'i','d'>>::value << "\n";std::cout << "name = " << row.field<std::string, name_literal<'n','a','m','e'>>::value << "\n";return 0;
}
说明
name_literal
模拟名字字面量(id
,name
等)field<T, Name>
定义带名字的字段成员row_t<Fields...>
用多重继承组合字段,实现复合数据结构statement_t
以 variadic mixin 形式继承Policy的成员和方法select_t
、no_where_t
定义了部分行为与数据select(...)
返回一个默认空状态的statement- 简单
where
演示,实际sqlpp11会生成全新statement类型链
关于 automatch 示例
// 一个简单automatch的条件检测例子
template<typename Tag>
bool andequalOrZero(const auto& a, const auto& b) {// 伪代码,真实会用Tag提取成员并比较return true; // 简化示例
}
你可以像你写的那样用:
auto automatch = [product](const auto& cand) {return true&& andequalOrZero<currency>(cand.first, product)&& andequalOrZero<paymentMethodId>(cand.first, product)&& andequalOrZero<subPaymentMethodId>(cand.first, product)&& andequalOrZero<merchantHistoryId>(cand.first, product)&& andequalOrZero<industryId>(cand.first, product)&& andequalOrZero<countryId>(cand.first, product);
};
几个关键模块补充成一个更完整的示例,包含代码和功能说明。
1. name_literal 示例(用作模板参数的“名字”)
// 通过 struct 作为名字标识符
template<char... Cs>
struct name_literal {static constexpr char value[sizeof...(Cs) + 1] = { Cs..., '\0' };
};
template<char... Cs>
constexpr char name_literal<Cs...>::value[sizeof...(Cs) + 1];
// 辅助宏方便写 name_literal
#define NAME_LITERAL(s) []{ \constexpr const char* str = s; \return name_literal<str[0], str[1], str[2], str[3], str[4], str[5], str[6], str[7]>{}; \
}()
// 例子:表示名字 "id"
using id = name_literal<'i', 'd'>;
using name = name_literal<'n', 'a', 'm', 'e'>;
功能:用类型来表示名字,实现模板中的名字参数,类似“字符串常量作为模板参数”。
2. field 模板 — 带名字和类型的字段
template<typename T, typename Name>
struct field {using type = T;using name = Name;T value;// 构造field(const T& val) : value(val) {}
};
// 使用示例
using id_field = field<int, id>;
using name_field = field<std::string, name>;
功能:让字段类型携带名字信息,方便后续反射或成员访问。
3. variadic_members(多成员)组合,简化成员定义
template<typename... Fields>
struct row_t : Fields... {row_t(const Fields&... fields) : Fields(fields)... {}// 通过名字访问字段,比如 get<id>(row)template<typename Name>auto& get();// 特化访问template<> int& get<id>() { return id_field::value; }template<> std::string& get<name>() { return name_field::value; }
};
功能:用多个带名字的字段组合成一条记录(行),并支持名字访问。
4. no_where_t 策略与 mixin 方法实现
struct no_where_t {template<typename Policies>struct _methods_t {template<typename... Args>auto where(Args... args) const {// 返回一个新的 statement,加入 where 条件return new_statement<Policies, no_where_t, where_t<void, Args...>>(static_cast<const derived_statement_t<Policies>&>(*this),where_data_t<void, Args...>{args...});}};
};
功能:定义没有 where 条件时,调用 where(...)
返回带 where 的新 statement。
5. statement_t 模板结合多个 Policy
template<typename Db, typename... Policies>
struct statement_t :public expression_operators<statement_t<Db, Policies...>, value_type_of<Policies...>>,public detail::statement_policies_t<Db, Policies...>::_result_methods_t,public Policies::template _member_t<detail::statement_policies_t<Db, Policies...>>...,public Policies::template _methods_t<detail::statement_policies_t<Db, Policies...>>...
{// 构造与实现省略
};
功能:用多继承将多个 Policy 混合,形成复杂的 SQL 语句类。
6. select 相关工厂函数示例
template<typename Database>
using blank_select_t = statement_t<Database,select_t,no_select_flag_list_t,no_select_column_list_t,no_from_t,no_where_t,no_group_by_t,no_having_t,no_order_by_t,no_limit_t,no_offset_t>;
template<typename... Columns>
auto select(Columns... columns) -> decltype(blank_select_t<void>().columns(columns...)) {return blank_select_t<void>().columns(columns...);
}
功能:返回一个空的 Select 语句,调用 columns(...)
来填充字段。
7. 自动匹配函数示例
template<typename FieldName, typename ProductType, typename CandidateType>
bool andequalOrZero(const CandidateType& cand, const ProductType& product) {const auto& candVal = get<FieldName>(cand.first);const auto& prodVal = get<FieldName>(product);return candVal == prodVal || candVal == 0;
}
// 用法示例:
auto automatch = [product](const auto& cand) {return andequalOrZero<currency>(cand, product)&& andequalOrZero<paymentMethodId>(cand, product)&& andequalOrZero<subPaymentMethodId>(cand, product)&& andequalOrZero<merchantHistoryId>(cand, product)&& andequalOrZero<industryId>(cand, product)&& andequalOrZero<countryId>(cand, product);
};
完整的例子
#include <iostream>
#include <string>
#include <type_traits>
// ---------- 1. name_literal ----------
template <char... Cs>
struct name_literal {static constexpr char value[sizeof...(Cs) + 1] = {Cs..., '\0'};static constexpr const char* c_str() { return value; }
};
template <char... Cs>
constexpr char name_literal<Cs...>::value[sizeof...(Cs) + 1];
using id = name_literal<'i', 'd'>;
using name = name_literal<'n', 'a', 'm', 'e'>;
using currency = name_literal<'c', 'u', 'r', 'r', 'e', 'n', 'c', 'y'>;
using paymentMethodId =name_literal<'p', 'a', 'y', 'm', 'e', 'n', 't', 'M', 'e', 't', 'h', 'o', 'd', 'I', 'd'>;
using subPaymentMethodId = name_literal<'s', 'u', 'b', 'P', 'a', 'y', 'm', 'e', 'n', 't', 'M', 'e','t', 'h', 'o', 'd', 'I', 'd'>;
using merchantHistoryId = name_literal<'m', 'e', 'r', 'c', 'h', 'a', 'n', 't', 'H', 'i', 's', 't','o', 'r', 'y', 'I', 'd'>;
using industryId = name_literal<'i', 'n', 'd', 'u', 's', 't', 'r', 'y', 'I', 'd'>;
using countryId = name_literal<'c', 'o', 'u', 'n', 't', 'r', 'y', 'I', 'd'>;
// ---------- 2. field ----------
template <typename T, typename Name>
struct field {using type = T;using name = Name;T value;field() = default;field(const T& val) : value(val) {}
};
// ---------- 3. type_of_impl ----------
template <typename Name, typename... Fields>
struct type_of_impl;
template <typename Name, typename T, typename FieldName, typename... Rest>
struct type_of_impl<Name, field<T, FieldName>, Rest...> {using type = std::conditional_t<std::is_same_v<Name, FieldName>, T,typename type_of_impl<Name, Rest...>::type>;
};
template <typename Name>
struct type_of_impl<Name> {using type = void;
};
// ---------- 4. row_t ----------
template <typename... Fields>
struct row_t : Fields... {row_t() = default;row_t(const Fields&... fields_type) : Fields(fields_type)... {}template <typename Name>auto& get() {using T = typename type_of_impl<Name, Fields...>::type;static_assert(!std::is_same_v<T, void>, "Field name not found in row_t");return static_cast<field<T, Name>&>(*this).value;}template <typename Name>const auto& get() const {using T = typename type_of_impl<Name, Fields...>::type;static_assert(!std::is_same_v<T, void>, "Field name not found in row_t");return static_cast<const field<T, Name>&>(*this).value;}
};
// ---------- 5. no_where_t ----------
struct no_where_t {template <typename Policies, typename Cur>struct _methods_t {template <typename... Args>const Cur& where(Args... args) const {std::cout << "where clause called with " << sizeof...(args) << " args\n";return static_cast<const Cur&>(*this);}};template <typename Policies>struct _member_t {struct type {};};
};
// ---------- 6. select_t ----------
struct select_t {template <typename Policies>struct _member_t {using type = row_t<field<int64_t, id>, field<std::string, name>>;};template <typename Policies, typename Cur>struct _methods_t {template <typename... Columns>const Cur& columns(Columns...) const {std::cout << "select columns count: " << sizeof...(Columns) << "\n";return static_cast<const Cur&>(*this);}};
};
// ---------- 7. statement_t ----------
template <typename Db, typename... Policies>
struct statement_t : Policies::template _member_t<void>::type...,Policies::template _methods_t<void, statement_t<Db, Policies...>>... {using self = statement_t<Db, Policies...>;using select_t::template _methods_t<void, self>::columns;using no_where_t::template _methods_t<void, self>::where;void print() const { std::cout << "statement_t with multiple policies\n"; }
};
// ---------- 8. 工厂函数 ----------
template <typename Db>
using blank_select_t = statement_t<Db, select_t, no_where_t>;
template <typename... Columns>
auto select(Columns... columns) {return blank_select_t<void>().columns(columns...);
}
// ---------- 9. compare_field ----------
template <typename FieldName, typename ProductType, typename CandidateType>
bool compare_field(const CandidateType& cand, const ProductType& product) {using T =typename type_of_impl<FieldName, field<int64_t, id>, field<std::string, name>,field<int64_t, currency>, field<int64_t, paymentMethodId>,field<int64_t, subPaymentMethodId>, field<int64_t, merchantHistoryId>,field<int64_t, industryId>, field<int64_t, countryId>>::type;if constexpr (std::is_same_v<T, int64_t>) {return cand.template get<FieldName>() == 0 ||cand.template get<FieldName>() == product.template get<FieldName>();} else {return cand.template get<FieldName>() == product.template get<FieldName>();}
}
// ---------- 10. main ----------
int main() {auto stmt = select("id", "name").where("id > 42");stmt.print();row_t<field<int64_t, id>, field<std::string, name>, field<int64_t, currency>,field<int64_t, paymentMethodId>, field<int64_t, subPaymentMethodId>,field<int64_t, merchantHistoryId>, field<int64_t, industryId>, field<int64_t, countryId>>row;row.get<id>() = 123;row.get<name>() = "Alice";row.get<currency>() = 1;row.get<paymentMethodId>() = 2;row.get<subPaymentMethodId>() = 3;row.get<merchantHistoryId>() = 4;row.get<industryId>() = 5;row.get<countryId>() = 100;std::cout << "row.id = " << row.get<id>() << "\n";std::cout << "row.name = " << row.get<name>() << "\n";auto product = row;auto automatch = [product](const auto& cand) {return compare_field<currency>(cand, product) &&compare_field<paymentMethodId>(cand, product) &&compare_field<subPaymentMethodId>(cand, product) &&compare_field<merchantHistoryId>(cand, product) &&compare_field<industryId>(cand, product) && compare_field<countryId>(cand, product);};std::cout << "automatch result: " << automatch(row) << "\n";return 0;
}
输出
select columns count: 2
where clause called with 1 args
statement_t with multiple policies
row.id = 123
row.name = Alice
automatch result: 1