news 2026/5/3 0:51:14

CppCon 2024 学习:Hiding your Implementation Details

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CppCon 2024 学习:Hiding your Implementation Details

信息隐藏(Information Hiding)比看起来难得多——理解

信息隐藏是软件工程的核心原则之一,但在实际开发中很难落实。指出软件工程教育标准普遍低于其他工程学科,因此开发者常常难以正确应用信息隐藏。此外,还有一些常见原因导致信息隐藏失败:

1⃣ 流程图本能(The Flowchart Instinct)

  • 很多程序员习惯从流程图出发设计程序,直接把控制流写死。
  • 问题:这种方法把实现细节暴露给外部,违反信息隐藏原则。
  • 理解公式化:
    程序结构≈控制流主导⇒模块内部实现暴露 \text{程序结构} \approx \text{控制流主导} \Rightarrow \text{模块内部实现暴露}程序结构控制流主导模块内部实现暴露

2⃣ 为变化做计划似乎是“过度计划”(Planning for Change Seems like too much Planning)

  • 开发者倾向于只做眼前需求,而忽略为未来变化设计抽象层。
  • 信息隐藏要求对可能变化的部分做封装,但这在短期项目中往往被忽略。
  • 逻辑关系:
    不为变化设计⇒系统脆弱⇒修改困难 \text{不为变化设计} \Rightarrow \text{系统脆弱} \Rightarrow \text{修改困难}不为变化设计系统脆弱修改困难

3⃣ 坏模型(Bad Models)

  • 如果系统模型设计不合理(例如对象、模块关系混乱),信息隐藏无法实现。
  • 坏模型会导致:
    • 模块耦合度高
    • 内部实现暴露
  • 可以表示为:
    坏模型⇒高耦合⇒信息暴露 \text{坏模型} \Rightarrow \text{高耦合} \Rightarrow \text{信息暴露}坏模型高耦合信息暴露

4⃣ 扩展坏的软件(Extending Bad Software)

  • 当基础软件设计不合理时,后续的功能扩展会增加复杂性,使得信息隐藏更难。
  • 核心问题:累积的设计缺陷导致模块边界混乱。
  • 表示:
    坏软件基础+扩展⇒信息隐藏困难 \text{坏软件基础} + \text{扩展} \Rightarrow \text{信息隐藏困难}坏软件基础+扩展信息隐藏困难

5⃣ 假设常常不被注意(Assumptions often go unnoticed)

  • 开发者对系统的隐含假设往往未显式表达。
  • 例子:作者的 KwIC Index 中存在重大缺陷,就是因为一些假设未被发现。
  • 表示:
    隐含假设∉文档/接口⇒隐藏信息被破坏 \text{隐含假设} \not\in \text{文档/接口} \Rightarrow \text{隐藏信息被破坏}隐含假设文档/接口隐藏信息被破坏

6⃣ 将系统环境反映在软件结构中(Reflecting the system environment in the software structure)

  • 一些程序设计直接依赖操作系统、硬件等环境,而非抽象接口。
  • 问题:模块内部实现暴露,使信息隐藏原则失效。
  • 表示:
    环境依赖⇒实现暴露⇒信息隐藏失败 \text{环境依赖} \Rightarrow \text{实现暴露} \Rightarrow \text{信息隐藏失败}环境依赖实现暴露信息隐藏失败

7⃣ “哦,太糟了,我们改了它”(“Oh, too bad we changed it”)

  • 当系统变化时,如果设计没有良好封装,会导致大面积修改。
  • 体现信息隐藏失败的直接后果。
  • 表示:
    设计未隐藏变化点+需求变化⇒系统破坏 \text{设计未隐藏变化点} + \text{需求变化} \Rightarrow \text{系统破坏}设计未隐藏变化点+需求变化系统破坏

总结

信息隐藏失败的原因大致可以归纳为:

  1. 设计思想问题(流程图本能、坏模型、坏软件扩展)
  2. 对变化缺乏前瞻(过少规划、未隐藏变化点)
  3. 假设未明确、过度依赖环境
    核心思想:
    信息隐藏=对变化点封装+抽象设计+假设显式化 \text{信息隐藏} = \text{对变化点封装} + \text{抽象设计} + \text{假设显式化}信息隐藏=对变化点封装+抽象设计+假设显式化

实现细节隐藏(Hiding Implementation Details)并不简单——理解

隐藏实现细节是软件设计的重要原则,但实际中很难完全做到。原因大致分为四类:

1⃣ 因为我们懒惰,或者认为不重要(Lazy or Not Important)

  • 很多开发者在时间紧迫时会舍弃“优雅设计”,直接使用暴露的实现细节。
  • 心态可能包括:
    • 时间压力:没有时间做抽象设计或封装。
    • 认为不重要:觉得将来可以轻松修改(现实往往不可能或很困难)。
  • 例子:
std::pair<First,Second>p;autok=p.first;autov=p.second;
  • 尽管存在其他安全的访问方法,但直接访问firstsecond仍然暴露了私有字段。
  • 核心思想:
    懒惰 + 时间压力⇒直接暴露实现细节 \text{懒惰 + 时间压力} \Rightarrow \text{直接暴露实现细节}懒惰+时间压力直接暴露实现细节
  • 演讲目的:劝告开发者不要懒惰,即使看似不重要,也要重视信息隐藏。

2⃣ 因为我们没有意识到自己暴露了私有细节(Unaware of Exposing Details)

  • 开发者往往在提供接口时,无意中泄露了私有信息。
  • 例子:
classFoo{std::map<std::string,int>properties;public:conststd::map<std::string,int>&getProperties()const;};
  • 暴露的私有细节:返回的std::map引用允许外部直接访问和修改内部数据。
  • 逻辑关系:
    返回内部引用⇒外部可修改私有数据⇒信息隐藏失败 \text{返回内部引用} \Rightarrow \text{外部可修改私有数据} \Rightarrow \text{信息隐藏失败}返回内部引用外部可修改私有数据信息隐藏失败

3⃣ 技术约束(Technical Constraints)

  • 有些语言或编译器限制,使得理想的封装方式难以实现。
  • 例子:
classFoo{Foo(intvalue):value(value){}// 想让构造函数私有,但编译不通过public:staticstd::unique_ptr<Foo>create(intvalue){returnstd::make_unique<Foo>(value);}};autofoo=Foo::create(7);
  • 解决办法:技术约束通常可以找到替代方案,如工厂函数、智能指针等。
  • 核心思想:
    技术限制≠无法隐藏⇒有解决方案可用 \text{技术限制} \neq \text{无法隐藏} \Rightarrow \text{有解决方案可用}技术限制=无法隐藏有解决方案可用

4⃣ 权衡折中(Tradeoffs)

  • 有时为了性能或便利,需要暴露部分实现细节:
    • 避免虚函数调用开销。
    • 提供随机访问接口以提升性能。
  • 需要进行权衡:
    • 某些场景下妥协是合理的。
    • 也可能存在两全其美的方案。
  • 逻辑表示:
    暴露数据⇔性能提升但需权衡设计原则 \text{暴露数据} \Leftrightarrow \text{性能提升} \quad \text{但需权衡设计原则}暴露数据性能提升但需权衡设计原则

总结公式化

隐藏实现细节失败的原因可以表示为:
信息隐藏失败=懒惰/不重视+无意识暴露+技术约束+设计折中 \text{信息隐藏失败} = \text{懒惰/不重视} + \text{无意识暴露} + \text{技术约束} + \text{设计折中}信息隐藏失败=懒惰/不重视+无意识暴露+技术约束+设计折中

  • 懒惰/不重视→ 时间压力、认为不重要
  • 无意识暴露→ 返回内部引用、直接访问内部字段
  • 技术约束→ 语言/编译器限制,但可解决
  • 设计折中→ 性能或便利导致暴露

目标(Our Goal)

在这个例子中,我们想要:

  1. 分析代码示例
    • 观察如何暴露实现细节(implementation details)。
  2. 理解为什么这是不好的设计
    • 每个例子中,设计问题是什么。
  3. 提出改进方案
    • 隐藏实现细节,同时保留所需功能。

std::pair 示例

问题:未隐藏私有成员是不正确的设计

  • 观察
    std::pair.first std::pair.second
    • std::pairfirstsecond是 public 的,这是 C++ 标准库中的设计“意外”(language accident)。
  • 为什么是问题
    • 暴露数据成员无法控制行为。比如:
      • 如果希望创建一个“懒计算”的second(如它是first的平方),直接访问 public 成员会破坏这种能力。
  • 信息隐藏原则
    成员应为 private⇒通过 getter/setter 或函数访问 \text{成员应为 private} \Rightarrow \text{通过 getter/setter 或函数访问}成员应为private通过getter/setter或函数访问

例子分析

// 假设函数依赖于 PAIR 有 first 和 second 字段template<typenamePAIR>std::ostream&operator<<(std::ostream&out,constPAIR&pair){returnout<<pair.first<<' '<<pair.second;}// 使用 std::pairautostdpair=std::make_pair("bye",42);std::cout<<stdpair<<std::endl;// 如果希望 second 惰性计算?autosquarepair=SquarePair(7);std::cout<<squarepair<<std::endl;

问题点

  1. operator<< 模板依赖 public 字段
    • 这意味着只要PAIRfirstsecond,无论它是 std::pair 还是自定义类型都可以被输出。
    • 但如果我们想让second惰性计算的(如first*first),就必须隐藏内部实现,否则会被外部直接访问。
  2. 扩展能力受限
    • 直接暴露字段会破坏对未来行为的控制。
    • 无法插入逻辑(如延迟求值、缓存结果、通知等)。

改进设计方向

  1. 将数据成员设为 private
classSquarePair{private:intfirst_;intsecond_;// 可以懒计算public:SquarePair(intx):first_(x),second_(0){}intfirst()const{returnfirst_;}intsecond()const{if(second_==0)second_=first_*first_;returnsecond_;}friendstd::ostream&operator<<(std::ostream&out,constSquarePair&p){returnout<<p.first()<<' '<<p.second();}};
  1. 好处
    • 外部无法直接访问second_,保证惰性计算逻辑不被破坏。
    • 保持接口一致性:通过函数访问,而不是直接访问成员。

总结公式化

  • 问题公式化
    公开成员⇒无法控制行为⇒设计不灵活 \text{公开成员} \Rightarrow \text{无法控制行为} \Rightarrow \text{设计不灵活}公开成员无法控制行为设计不灵活
  • 改进公式化
    成员 private+getter/setter 或友元函数⇒实现细节隐藏∧可扩展性 \text{成员 private} + \text{getter/setter 或友元函数} \Rightarrow \text{实现细节隐藏} \wedge \text{可扩展性}成员private+getter/setter或友元函数实现细节隐藏可扩展性

结构体与类设计的一般问题(structs, in general)

1. 基本 struct 示例

structOrder{Price price;Quantity quantity;};
  • 问题
    • struct默认是public,所有成员都公开。
    • 当需求变化时(比如支持不同类型的订单),直接访问成员会导致修改困难。
    • 难以扩展,无法在不破坏已有接口的情况下增加逻辑(如惰性计算、验证、转换)。

2. 支持泛型订单(Supporting Generic Orders)

classOrder{Price price;Quantity quantity;public:// 构造函数略PricegetPrice()const{returnprice;}QuantitygetQuantity()const{returnquantity;}};
  • 改进点
    • 将成员私有化(private)。
    • 提供 getter 函数访问。
  • 好处
    • 可以在 getter 中加入逻辑(如验证、缓存、转换)。
    • 支持“静态多态(Static Polymorphism)”或“鸭子类型(Duck Typing)”,允许不同类实现同样接口。

3. 更复杂的订单类型

classTimedOrder{std::vector<std::pair<chrono::time_point,Price>>price_limits;Quantity quantity;public:PricegetPrice()const{/* 获取合适价格 */}QuantitygetQuantity()const{returnquantity;}};
  • 说明
    • 支持价格随时间变化的订单。
    • 依然可以通过接口(getter)与普通Order兼容。

4. const 成员的问题(Just a const member)

classPerson{std::string name;public:constId id;Person(std::string name,Id id):name(std::move(name)),id(id){}};
  • 问题
    • const成员一旦初始化就不能修改。
    • 需求可能会变化,例如:
      • 需要支持多种身份证明(SSN、护照等)。
      • 可能需要更新护照信息,同时保留旧护照号。
  • 结论
    • 直接用const绑定会限制未来扩展。
    • 正确的做法是将成员私有化,并提供可控的访问与修改接口。

5. 最终改进设计

classPerson{std::string name;Id id;// 不再是 constpublic:Person(std::string name,Id id):name(std::move(name)),id(id){}// 提供接口操作 IdvoidnewPassport(PassportDetails pd){id.update(pd);}constId&getId()const{returnid;}};
  • 好处
    • 支持未来需求变化。
    • 保证实现细节隐藏,同时提供必要接口。
    • 提供封装和灵活性。

6. 总结与经验(Takeaway)

  1. 始终将数据成员(包括 static 成员)设为 private
  2. 提供受控访问接口:
    • Getter / Setter
    • 特定功能函数(如newPassport()
  3. 避免使用const限制未来需求扩展。
  4. 接口设计优先考虑可扩展性和信息隐藏。

原则公式化

  • 信息隐藏
    成员 private∧接口访问⇒可控扩展∧维护性强 \text{成员 private} \wedge \text{接口访问} \Rightarrow \text{可控扩展} \wedge \text{维护性强}成员private接口访问可控扩展维护性强
  • 错误做法
    成员 public 或 const⇒难以扩展∧实现细节泄露 \text{成员 public 或 const} \Rightarrow \text{难以扩展} \wedge \text{实现细节泄露}成员publicconst难以扩展实现细节泄露

1. API 使用与风险

  • Hyrum 定律

    “开发者会依赖接口的所有可观察特性和行为,即使这些行为没有在合同中声明。”

  • 含义
    • 当一个 API 有足够多的用户时,不论你承诺什么,所有可观察的行为都会被依赖。
    • 换句话说,API 的设计细节一旦暴露出来,就可能被外部代码绑定,难以修改。
  • 研究发现
    1. 应用通常只使用 API 的一小部分。
    2. 未使用的 API 更容易出错。
    3. API 选项过多(比如重载函数太多)容易被误用。
    4. 选项少不仅减少出错机会,也减少测试工作量。

2. API 设计原则:Lean & Mean

  1. 保持 API 简洁
    • 只添加真正需要的功能。
  2. 限制 API 的使用范围
    • 只暴露必要的参数和返回值。
    • 不泄露实现细节。
  3. 根据使用场景暴露不同视图
    • 不同上下文可能只需要部分数据。
    • 通过视图或包装类(Wrapper)暴露相关数据。

3. 上下文特定性(Context-specific)

  • 核心思想
    • 在某些使用场景下需要将成员或方法设为public
    • 在其他使用场景下,完全没有必要暴露。
  • 原则
    1. 如果用户不需要,就不要提供。
    2. 如果提供了,他们会使用。
    3. 如果使用了,他们可能会滥用。
    4. 不要轻易相信外部使用者。
  • 结论
    • 仅仅把成员放在private并不够。
    • API 设计必须考虑不同使用上下文,提供最窄的接口。

4. 如何传递参数,只暴露必要信息

  • 方法
    1. 值语义(Value semantics)
      • 直接传值,需要时可封装在类中。
    2. 接口(Interface)
      • 提供有限访问方法。
    3. Pimpl Idiom
      • 将实现隐藏在指针后面。
    4. 包装类(Wrapper/Views)
      • 提供只读或部分访问。
    5. C++20 Concepts
      • 用类型约束提供只暴露所需的接口。
  • 示例练习
    • Const Map, Mutable Vals
      • 初始化一个 map。
      • 传递方式:允许修改 map 的值,但 map 本身不能修改(如不能添加或删除 key)。
      • 可用包装类或概念来实现。

5. 核心 takeaway

  1. API 要尽量窄:只暴露必要信息。
  2. 上下文相关性决定什么可以暴露。
  3. 避免未使用的 API 或冗余接口。
  4. 技术手段可以帮助隐藏实现细节:
    • Pimpl
    • 只读视图
    • 类型约束
  5. 信任无外人,只暴露必需部分。

公式化理解

  • 上下文敏感暴露
    API exposed=f(context)where minimal exposure⇒less abuse, less bugs \text{API exposed} = f(\text{context}) \quad \text{where minimal exposure} \Rightarrow \text{less abuse, less bugs}API exposed=f(context)where minimal exposureless abuse, less bugs
  • Hyrum 定律的启示
    任何可观察行为⇒可能被依赖⇒修改风险 \text{任何可观察行为} \Rightarrow \text{可能被依赖} \Rightarrow \text{修改风险}任何可观察行为可能被依赖修改风险

1. 什么是 Concepts

  • 概念(Concept)
    1. 对模板参数的约束(constraint)。
    2. 在编译期求值为布尔值(boolean)。
    3. 可用于函数重载解析(overload resolution)。
  • 优点
    • Concepts 是 SFINAE(Substitution Failure Is Not An Error)的更优雅、可读性更高的替代方案。
    • 通过 Concepts,可以清晰表达“哪些类型允许被模板接受”。
  • 数学理解
    Concept(T)=true/false at compile-time \text{Concept}(T) = \text{true/false at compile-time}Concept(T)=true/false at compile-time
    表示模板参数是否满足约束。

2. 例子:函数模板重载

目标:实现两个print函数:

  1. 对可迭代类型(如std::vectorstd::array):
template<typenameIterable>voidprint(constIterable&iterable){for(constauto&v:iterable)std::cout<<v<<std::endl;}
  1. 对其他类型:
template<typenameT>voidprint(constT&t){std::cout<<t<<std::endl;}
  • 问题
    • 上述代码在模板重载解析时可能出错,因为编译器无法区分是否可迭代。
    • 需要 SFINAE 或 Concepts 来约束模板参数。

3. SFINAE 方案

template<typenameIterable,std::enable_if_t<is_iterable_v<Iterable>>*=nullptr>voidprint(constIterable&iterable){...}template<typenameT,std::enable_if_t<!is_iterable_v<T>>*=nullptr>voidprint(constT&t){...}
  • 问题
    • 写法冗长,不直观。
    • is_iterable_v不是语言内置,需要自定义。

4. Concepts 方案

方法 1:requires约束

template<typenameT>requiresstd::ranges::range<T>voidprint(constT&iterable){for(constauto&v:iterable)std::cout<<v<<std::endl;}template<typenameT>voidprint(constT&t){std::cout<<t<<std::endl;}
  • 说明
    • requires表达了模板参数必须满足std::ranges::range<T>概念。
    • 简洁明了,比 SFINAE 可读性高。

方法 2:约束模板类型

template<std::ranges::range T>voidprint(constT&iterable){...}
  • 将模板参数直接声明为满足某个概念的类型。

方法 3:约束函数参数(abbreviated function template)

voidprint(conststd::ranges::rangeauto&iterable){for(constauto&v:iterable)std::cout<<v<<std::endl;}voidprint(constauto&t){std::cout<<t<<std::endl;}
  • 特点
    • C++20 允许在函数参数使用auto,使函数成为模板函数。
    • 这种写法叫做“简化函数模板(abbreviated function template)”,最简洁。

5. 总结

  1. Concepts用于约束模板参数,提供更清晰、可维护的重载逻辑。
  2. 可以替代 SFINAE,使模板代码可读性更高。
  3. 对函数模板重载,可通过:
    • requires子句约束模板类型。
    • 模板参数直接约束(template<Concept T>)。
    • 函数参数约束(auto+ 概念)实现简化。
  4. 使用场景:比如打印函数、容器处理函数等,需要区分可迭代类型和其他类型。

1. 创建自定义 Concept

template<typenameT>conceptMeowable=requires(constT t){t.meow();// t 必须有一个 const 方法 meow};
  • 解释
    1. concept关键字用于定义概念Meowable
    2. requires(const T t)表示对类型T的约束条件:
      • 这里要求T类型的对象t必须有一个可调用的meow()方法,并且该方法是const的。
    3. 如果T满足这个条件,则Meowable<T>true,否则为false
  • 数学理解
    Meowable(T) ⟺ ∃t:T, t.meow() 是合法的且 const \text{Meowable}(T) \iff \exists t: T, \text{ t.meow() 是合法的且 const }Meowable(T)t:T,t.meow()是合法的且const

2. 定义类型满足 Concept

structCat{voidmeow()const{std::cout<<"mewo\n";}};structFrenchCat{voidmeow()const{std::cout<<"miaou\n";}};
  • 说明
    • CatFrenchCat都有一个const方法meow(),所以它们满足Meowable概念。
    • 如果某个类型没有meow()方法,或者meow()不是const,就不满足该概念。

3. 使用 Concept 约束函数模板

voiddo_meow(constMeowableauto&meowable){meowable.meow();}
  • 解释
    1. const Meowable auto&简化函数模板(abbreviated function template)的写法。
    2. 仅允许满足Meowable概念的类型作为参数传入。
    3. 编译器在调用时会检查类型是否满足概念,如果不满足,则编译失败。
  • 例子调用
Cat c;do_meow(c);// OK, Cat 满足 Meowabledo_meow(FrenchCat{});// OK, FrenchCat 满足 Meowable// do_meow(7); // 编译错误, int 不满足 Meowable

4. 编译期静态断言

static_assert(Meowable<Cat>);static_assert(!Meowable<int>);static_assert(Meowable<FrenchCat>);
  • 作用
    • static_assert在编译期检查类型是否满足概念。
    • 可以作为测试,保证模板约束的正确性。
    • 如果断言失败,编译器报错。

5. 优势总结

  1. 清晰表达意图
    • Meowable直接描述了“可以喵”的类型,而不需要手动写 SFINAE。
  2. 编译期安全
    • 不符合概念的类型直接编译错误,避免运行期错误。
  3. 可组合性
    • Concepts 可以组合,如Meowable && Serializable等。
  4. 简化模板重载
    • auto配合使用,使模板函数声明简洁:
    voiddo_meow(constMeowableauto&meowable);
  5. 静态断言支持
    • static_assert可以直接用概念检查类型是否符合约束。

6. 扩展

  • 可以定义更复杂的概念,例如要求方法返回特定类型:
template<typenameT>conceptMeowableInt=requires(constT t){{t.meow()}->std::same_as<int>;};
  • 这里要求t.meow()的返回类型必须是int
    总结:
  • 概念(Concept)是 C++20 强大的模板约束工具。
  • 自定义概念可以约束类型成员函数、操作符等。
  • 简化函数模板+Concepts能让模板代码既安全又可读。

1.requires的两种用法

在 C++20 中,requires主要有两种用途:

  1. requires子句(requires clause)
    • 用于模板参数或函数声明,指定约束条件。
    • 语法:
    template<typenameT>requiresAddable<T>voidfoo(T t){...}
    • 含义:模板参数T必须满足Addable<T>概念或布尔常量表达式,否则编译失败。
  2. requires表达式(requires expression)
    • 用于定义自定义概念,描述某种约束。
    • 语法:
    template<classT>conceptFooable=requires(T t){t.foo();// T 必须有 foo() 方法};

2. requires 子句约束条件类型

requires子句必须是一个可以在编译期求值为bool的常量表达式,包括:

  1. constexpr bool
    template<typenameT>voidfoo(T t)requiresfalse{}// 永远不满足
  2. 布尔常量表达式
    • 例如:
    template<typenameT>voidfoo(T t)requires(sizeof(T)<=4){}// 限制类型大小
  3. 基于类型特性的布尔值
    #include<type_traits>template<typenameT>voidfoo(T t)requiresstd::is_integral_v<T>{}// 必须是整数类型
  4. 已有概念
    #include<concepts>template<typenameT>voidfoo(T t)requiresstd::integral<T>{}// T 必须是整数概念类型
  5. 复合约束(逻辑与/或/非)
    template<typenameT>voidfoo(T t)requires(std::integral<T>&&sizeof(T)==1){}// T 必须是整数且大小为 1 字节(char 或 bool)

3. 示例分析

示例 1:类型为整数才允许调用

template<typenameT>constexprboolis_int=false;template<>constexprboolis_int<int>=true;template<typenameT>voidfoo(T t)requiresis_int<T>{}intmain(){foo(8);// ok, T=int// foo(""); // fails, T=const char* 不满足 is_int}
  • is_int<T>是一个布尔常量表达式。
  • requires子句会在编译期判断T是否满足条件。
  • 不满足时,编译器直接报错。

示例 2:限制类型大小

template<typenameT>voidfoo(T t)requires(sizeof(T)<=4){}intmain(){foo(8);// ok, sizeof(int)<=4// foo(""); // fails, sizeof(const char*)=8}
  • requires子句可以使用常量表达式计算类型大小
  • 避免运行时出错。

示例 3:组合约束

#include<concepts>template<typenameT>voidfoo(T t)requires(std::integral<T>&&sizeof(T)==1){}intmain(){foo('a');// ok, charfoo(false);// ok, bool// foo(8); // fails, int size>1// foo(""); // fails, const char* 不满足整数概念}
  • 复合约束可用逻辑与&&/ 或||组合多个条件。
  • 语义非常直观:类型必须同时满足所有约束。

4. 总结

  1. requires子句
    • 用于模板或函数声明,限定类型必须满足条件。
    • 可以使用布尔常量表达式、类型特性或概念。
  2. requires表达式
    • 用于自定义概念,约束类型必须有特定成员或操作符。
  3. 优势
    • 编译期检查类型,避免运行期错误。
    • 替代复杂的 SFINAE,语法清晰。
    • 可组合和复用,增强模板代码的可读性和安全性。
  4. 数学形式
    假设C(T)为约束条件,则:
    T 可以使用函数 foo ⟺ C(T)=true T \text{ 可以使用函数 foo } \iff C(T) = \text{true}T可以使用函数fooC(T)=true
  • 示例:C(T) = (\text{std::integral<T>} \wedge \text{sizeof(T)}=1)
    foo(T) 仅当 T∈char,bool foo(T) \text{ 仅当 } T \in { char, bool }foo(T)仅当Tchar,bool

1.requires expression

requires expression是定义概念(concept)的一种方式,用于描述某个类型必须满足的操作或成员存在性。

template<classT>conceptFooable=requires(T t){t.foo();// 类型 T 必须有一个 foo() 方法};
  • 上面代码定义了一个概念Fooable
  • requires (T t)中的语句是对类型 T 的约束
  • 如果 T 不满足约束(比如没有foo()方法),编译时就会报错。
  • 注意:requires expression并不是定义概念的唯一方法,后面还有其他方式(如布尔常量表达式或组合概念)。

2. 定义概念的基本方式

概念可以通过多种方式约束模板参数:

  1. 布尔常量表达式
    #include<concepts>template<typenameT>conceptByteSize=(sizeof(T)==1);// T 的大小必须为 1 字节template<typenameT>voidfoo(T t)requiresByteSize<T>{}intmain(){foo('a');// okfoo(false);// ok// foo(8); // fails// foo(""); // fails}
    • ByteSize<T>是一个布尔表达式概念。
    • 满足条件的类型才能调用foo
  2. 组合概念(Conjunction of concepts)
    可以把已有概念和类型特性组合:
    #include<concepts>template<typenameT>conceptSignedByteSize=(std::is_signed_v<T>&&(sizeof(T)==1));template<typenameT>voidfoo(T t)requiresSignedByteSize<T>{}intmain(){foo('a');// ok(char 是否有符号取决于编译器)// foo(false); // fails// foo(8); // fails// foo(""); // fails}
    • SignedByteSize限制类型既是有符号类型,又是1 字节大小
    • 通过逻辑与&&可以组合多个约束条件。
  3. 组合现有概念与类型操作
    可以将已有概念与类型值进行组合,例如检查某个容器是否为特定元素类型:
    #include<vector>#include<ranges>#include<concepts>usingnamespacestd::ranges;template<typenameR,typenameE>conceptRangeOf=range<R>&&std::same_as<range_value_t<R>,E>;template<RangeOf<int>T>voidfoo(constT&t){}intmain(){std::vector<int>vec_int={1,2};std::vector<char>vec_char={'a','b'};foo(vec_int);// ok// foo(vec_char); // fails// foo(8); // fails// foo(""); // fails}
    • RangeOf<R, E>表示类型R是一个范围(range)并且元素类型是E
    • 对于不符合条件的类型,编译期直接报错。

3. 概念的作用

  • 编译期约束类型,保证模板安全性。
  • 避免传统 SFINAE 的复杂语法,更直观清晰。
  • 可以组合不同约束,实现高度灵活的模板接口。
  • 概念可用作requires子句auto简化模板函数

4. 数学表示

设一个概念C(T),则:
T 可以使用函数 foo ⟺ C(T)=true T \text{ 可以使用函数 } foo \iff C(T) = \text{true}T可以使用函数fooC(T)=true

  • 例如:
    ByteSize(T)=(sizeof(T)=1) ByteSize(T) = (sizeof(T) = 1)ByteSize(T)=(sizeof(T)=1)
    SignedByteSize(T)=(std::is_signed_v<T>∧sizeof(T)=1) SignedByteSize(T) = (\text{std::is\_signed\_v<T>} \wedge sizeof(T) = 1)SignedByteSize(T)=(std::is_signed_v<T>sizeof(T)=1)
  • 调用foo(T)的条件就是概念约束成立。

1. 基本语法

概念可以通过requires <requirements>定义,其中<requirements>是对类型或表达式的约束条件:

template<classT>conceptFooable=requires<requirements>;
  • requirements可以采用两种形式:
    1. 花括号体(Curly body):不带参数,仅说明类型必须满足的要求。
    2. 带参数列表 + 花括号体:要求类型 T 满足特定操作或成员函数存在。
  • 这里的arguments are unevaluated:在requires中的参数不会真的求值,只检查类型是否有相关成员或操作。

2. 示例解析

示例 1:静态成员函数

#include<concepts>template<typenameT>conceptFooable=requires{T::foo();};template<typenameT>voidfoo(T t)requiresFooable<T>{t.foo();}intmain(){structFoo{staticvoidfoo(){};};foo(Foo{});// ok// foo(8); // fails// foo(""); // fails}
  • requires { T::foo(); }表示T 类型必须有一个静态成员函数foo()
  • 编译器仅检查T::foo()是否存在,不会调用它。
  • 传入的Foo满足条件,因此foo(Foo{})可以编译。
  • 其他类型(如intconst char*)不满足条件,会在编译期报错。

示例 2:普通成员函数

#include<concepts>template<typenameT>conceptFooable=requires(T t){t.foo();};template<typenameT>voidfoo(T t)requiresFooable<T>{t.foo();}intmain(){structFoo{voidfoo(){};};foo(Foo{});// ok// foo(8); // fails// foo(""); // fails}
  • requires(T t)表示T 类型必须有一个普通成员函数foo()
  • 传入对象Foo{}可以调用成员函数foo(),因此通过概念约束。
  • 对不满足条件的类型(如int)直接编译报错。

示例 3:使用std::declval

#include<concepts>#include<utility>// for std::declvaltemplate<typenameT>conceptFooable=requires{std::declval<T>().foo();};template<typenameT>voidfoo(T t)requiresFooable<T>{t.foo();}intmain(){structFoo{voidfoo(){};};foo(Foo{});// ok// foo(8); // fails// foo(""); // fails}
  • std::declval<T>()可以生成类型 T 的右值引用,在编译期用于检查表达式有效性,而无需构造对象。
  • requires { std::declval<T>().foo(); }作用与示例 2 相似,但可以处理没有默认构造函数的类型。
  • 优点:不需要实例化对象即可检查成员函数是否存在。

3. 总结

  1. requires expression可以用于定义概念,检查类型是否满足某些操作或成员函数存在。
  2. 静态 vs 普通成员函数
    • 静态成员函数:requires { T::foo(); }
    • 普通成员函数:requires(T t) { t.foo(); }requires { std::declval<T>().foo(); }
  3. 编译期检查
    • 约束在编译期生效,不会真正调用成员函数。
    • 不满足约束的类型在模板实例化时直接报错。
  4. 优点
    • 比 SFINAE 更直观
    • 可以灵活组合约束
    • 支持静态或动态成员函数检查

4. 数学形式表示

  • 定义概念Fooable(T)Fooable(T)Fooable(T)
    Fooable(T)={true如果 T 有成员函数 foo()false否则 Fooable(T) = \begin{cases} true & \text{如果 T 有成员函数 foo()} \\ false & \text{否则} \end{cases}Fooable(T)={truefalse如果T有成员函数foo()否则
  • 模板函数约束:
    foo(T t) 可以调用 ⟺ Fooable(T)=true \text{foo(T t)} \text{ 可以调用 } \iff Fooable(T) = truefoo(T t)可以调用Fooable(T)=true
类型 T --> 概念约束检查 --> 满足条件调用 / 不满足报错

1. 概念主体的内容

在概念体 (concept body) 中,可以有多种约束形式:

  1. 未求值表达式(Unevaluated expressions)
    • 只需要能编译即可,不会真正执行。
    • 用于检查类型是否存在某个成员函数或操作。
  2. 内部requires子句
    • 用于要求类型满足一个布尔表达式。
    • 语法必须使用requires关键字。
  3. 花括号表达式(Curly-braced expressions)
    • 可以用于注入到其他概念中。
    • 支持返回类型约束,例如{T::bar()} -> std::same_as<int>;表示T::bar()的返回值类型必须是int
  4. 内部类型约束(Internal type existence)
    • 要求类型必须有内部类型,语法必须加typename
    • 例如typename T::inner_type;检查T是否有嵌套类型inner_type

2. 示例解析

template<classT>conceptAllSortOfChecks=requires{T::foo();// 1. 未求值表达式requiresT::Size==2;// 2. 内部 requires 子句{T::bar()}->std::same_as<int>;// 3. 返回类型约束typenameT::inner_type;// 4. 内部类型约束};
  • A 类型满足全部条件:
    • foo()存在
    • Size == 2
    • bar()返回int
    • inner_type类型
      因此AllSortOfChecks<A>true
  • B 类型不满足Size == 2AllSortOfChecks<B>false
  • C 类型不满足bar()返回类型为intAllSortOfChecks<C>false
  • D 类型Size不是constexpr→ 概念检查无效

3. 总结要点

  1. requires 关键字在概念中的两种用法
    • requires clause(在模板或函数声明上)
      template<typenameT>requiresSomeConcept<T>{...}
    • requires expression(概念内部)
      conceptC=requires(T t){t.foo();requiresT::Size==2;...};
  2. 概念中表达式未求值
    • 检查类型或成员是否存在,不会真正运行。
    • 适合检查静态成员、普通成员、内部类型或返回类型。
  3. 返回类型约束
    • 使用花括号表达式{expr} -> std::same_as<Type>;指定返回类型。
    • 可以保证模板参数符合预期接口。
  4. 内部类型约束
    • typename T::inner_type;用于检查是否存在嵌套类型。
    • 必须加typename,否则语法错误。

4. 数学形式表示

设概念AllSortOfChecks(T)定义如下:
AllSortOfChecks(T)={true,如果满足下列条件1.存在静态成员函数 T::foo()2.T::Size==23.T::bar() 返回类型为 int4.存在嵌套类型 T::innertypefalse,否则 AllSortOfChecks(T) = \begin{cases} true, & \text{如果满足下列条件} \\ & 1. \text{存在静态成员函数 } T::foo() \\ & 2. T::Size == 2 \\ & 3. T::bar() \text{ 返回类型为 } int \\ & 4. \text{存在嵌套类型 } T::inner_type \\ false, & \text{否则} \end{cases}AllSortOfChecks(T)=true,false,如果满足下列条件1.存在静态成员函数T::foo()2.T::Size==23.T::bar()返回类型为int4.存在嵌套类型T::innertype否则

  • 对每个类型 A、B、C、D,可以代入判断是否满足条件,从而决定static_assert是否通过。

概念的模板参数 (The concept’s param)

1. 概念总是模板化的

  • 每个概念 (concept) 至少要有一个模板参数。
  • 当概念直接引用某个类型时,编译器可以自动注入第一个模板参数。
示例:
// 1. 概念直接作用于模板类型参数template<Printable T>voidprint1(constT&t){cout<<t<<endl;}// 2. 概念作用于 auto 参数voidprint2(constPrintableauto&t){cout<<t<<endl;}
  • 解释:
    • Printable T会让编译器自动推导T的类型。
    • Printable auto& t是 C++20 中“缩写函数模板”(abbreviated function template)的语法,效果类似于模板化函数,但语法更简洁。

2. 概念参数需要手动提供

  • 如果概念不直接依赖模板参数类型,第一个参数不会被自动注入,需要显式指定。
示例:
template<typenameT>requiresPrintable<T>voidprint(constT&t){cout<<t<<endl;}ifconstexpr(Printable<T>){...}static_assert(Printable<int>);// OK
  • 在以上情况中,Printable<T>必须显式写出类型T

3. 多参数概念

  • 一个概念可以有多个模板参数。
  • 第一个参数可以自动注入,其余参数需要手动提供。
示例:DereferenceableDereferenceableTo
template<typenameT>conceptDereferenceable=requires(T t){*t;// t 必须可以解引用};template<typenameT,typenameValueType>conceptDereferenceableTo=Dereferenceable<T>&&std::same_as<std::remove_cvref_t<decltype(*std::declval<T>())>,ValueType>;
  • 说明:
    • Dereferenceable<T>:T 类型必须可以进行*t操作。
    • DereferenceableTo<T, ValueType>:除了可以解引用,还要求解引用后的值类型与ValueType相同。

4. 使用示例

voidprint(constauto&v){cout<<"Simple value: "<<v<<endl;}voidprint(constDereferenceableauto&t){cout<<"Dereferenceable, inner value: "<<*t<<", at: "<<t<<endl;}voidprint(constDereferenceableTo<char>auto&t){cout<<"Dereferenceable to char: "<<t<<", at: "<<(void*)t<<endl;}intmain(){inti=8;print(i);// Simple valueprint(&i);// Dereferenceable, inner valueconstchar*s="hello";print(s);// DereferenceableTo<char>charstr[]="hi";print(str);// DereferenceableTo<char>}
  • 解释:
    1. print(i):普通整型,匹配第一个函数。
    2. print(&i):指针类型,匹配Dereferenceable auto
    3. print(s)print(str):字符指针或字符数组,匹配DereferenceableTo<char>
  • 概念参数注入机制:编译器会自动推导auto类型,并将其作为概念的第一个模板参数。

5. 核心总结

  1. 概念模板参数
    • 至少有一个模板参数。
    • 第一个参数可以自动注入。
    • 多参数概念中,非第一个参数必须显式提供。
  2. 用途
    • 对模板参数添加约束。
    • 简化 SFINAE 代码,提高可读性。
    • 可在函数模板重载中精确匹配不同类型。
  3. 实践建议
    • 尽量使用auto缩写函数模板简化语法。
    • 对复杂类型需求使用多参数概念。
    • 使用requires明确约束逻辑,避免编译错误。

练习 1:Twople<First, Second>

目标

希望定义一个概念Twople<First, Second>,可以约束一个类型是“包含两个元素的容器”,并且第一个元素类型是First,第二个元素类型是Second,示例:

voidfoo(constTwople<int,double>auto&){...}

能够匹配:

  • std::pair<int, double>
  • std::tuple<int, double>

出错原因

你最初写的:

voidfoo(constTwople<int,double>auto&){...}

报错:

'Twople' does not name a type

原因:

  1. C++20 中,概念必须先定义(template<class ...> concept Twople = ...;)。
  2. 概念可以带模板参数,但要写成Twople<P, First, Second>或使用auto语法时,概念参数必须正确匹配函数模板的推导规则。

正确实现示例

#include<iostream>#include<tuple>#include<concepts>usingstd::cout;usingstd::endl;// Twople 概念template<typenameP,typenameFirst,typenameSecond>conceptTwople=requires(P p){requiresstd::tuple_size<P>::value==2;// 必须有两个元素{std::get<0>(p)}->std::convertible_to<First>;// 第一个元素类型{std::get<1>(p)}->std::convertible_to<Second>;// 第二个元素类型};// 使用概念voidfoo(constTwople<auto,int,double>auto&t){cout<<"Twople<int, double>\n";}voidfoo(constauto&){cout<<"const auto&\n";}intmain(){std::pair p1{1,2.5};foo(p1);// Twople<int, double>std::pair p2{1.5,2};foo(p2);// const auto&std::tuple tup{1,2.5};foo(tup);// Twople<int, double>}
  • 核心:
    • std::tuple_size<P>::value == 2判断元素个数。
    • std::get<0>(p)/std::get<1>(p)确认元素类型。
    • 使用auto和概念模板参数结合,使类型推导自动完成。

练习 2:OneOf<Ts...>

目标

定义一个概念OneOf<Ts...>,表示模板类型必须是指定类型之一:

voidfoo(constOneOf<char,int,long>auto&);voidfoo(constOneOf<double,float>auto&);

出错原因

之前报错:

'OneOf' does not name a type

原因:

  1. 概念没有定义。
  2. auto参数与概念模板参数没有正确结合。
  3. C++20 支持可变参数模板概念,用于匹配任意类型列表。

正确实现示例

#include<concepts>#include<iostream>usingstd::cout;usingstd::endl;// OneOf 概念实现template<typenameT,typename...Ts>conceptOneOf=(std::same_as<T,Ts>||...);// fold expression// 使用概念voidfoo(constOneOf<char,int,long>auto&){cout<<"OneOf<char, int, long>\n";}voidfoo(constOneOf<double,float>auto&){cout<<"OneOf<double, float>\n";}voidfoo(constauto&){cout<<"const auto&\n";}intmain(){foo(3.5);// OneOf<double, float>foo(4.5f);// OneOf<double, float>std::pair p{1,2};foo(p);// const auto&foo('a');// OneOf<char, int, long>foo(1);// OneOf<char, int, long>foo(2L);// OneOf<char, int, long>foo("hello");// const auto&}
  • 核心:
    • template<typename T, typename... Ts> concept OneOf = (std::same_as<T, Ts> || ...);
      • fold expression|| ...遍历可变模板参数列表,判断类型是否匹配。
    • 将概念与auto参数结合,实现模板函数的精确匹配。

总结

  1. Twople
    • 约束容器类型的元素数量和类型。
    • 支持std::pairstd::tuple
  2. OneOf
    • 用于多类型选择。
    • 使用 fold expression 判断类型是否匹配。
  3. C++20概念与 auto 函数参数结合非常灵活:
    • 可以实现类型安全的函数模板重载。
    • 避免复杂的 SFINAE 语法,提高可读性。

概念的非类型参数

核心思想

C++20 中的概念 (concepts)

  1. 第一个参数必须是类型type)。
  2. 后续参数可以是非类型参数(如整数、枚举、指针等)。
  3. 这允许我们在概念中加入编译期常量约束,实现更灵活的类型检查。

示例分析

template<classT,size_t MIN_SIZE,size_t MAX_SIZE>conceptSizeBetween=sizeof(T)>=MIN_SIZE&&sizeof(T)<=MAX_SIZE;
解释:
  1. T:类型参数
  2. MIN_SIZEMAX_SIZE:非类型参数(size_t编译期常量)
  3. 条件:
    MIN_SIZE≤sizeof(T)≤MAX_SIZE\text{MIN\_SIZE} \leq \text{sizeof}(T) \leq \text{MAX\_SIZE}MIN_SIZEsizeof(T)MAX_SIZE
  4. 如果类型T的字节大小在[MIN_SIZE, MAX_SIZE]区间内,则SizeBetween<T, MIN_SIZE, MAX_SIZE>true

函数模板结合概念

voidfoo(constSizeBetween<4,16>auto&){cout<<"SizeBetween<4, 16>\n";}voidfoo(constSizeBetween<17,32>auto&){cout<<"SizeBetween<17, 32>\n";}// 所有其他情况voidfoo(constauto&){cout<<"const auto&\n";}
分析:
  1. SizeBetween<4, 16> auto&表示:
    • 自动推导类型T
    • 前提是sizeof(T) ∈ [4, 16]
  2. SizeBetween<17, 32> auto&表示:
    • sizeof(T) ∈ [17, 32]
  3. 其余类型通过普通auto&匹配。

main 函数中的调用

intmain(){foo(4.5);// double, sizeof(double) = 8 → 匹配 SizeBetween<4,16>std::pair p{1,2};// pair<int,int>, sizeof(pair) ≈ 16 → 匹配 SizeBetween<4,16>std::tuple tup{1,2.5,8};// tuple<int,double,int>, sizeof(tuple) > 16 → 匹配 SizeBetween<17,32>?foo('a');// char, sizeof(char) = 1 → 匹配 default auto}
解释:
  • double→ 8 字节 →[4,16]→ 匹配第一个函数。
  • std::pair<int,int>→ 假设 16 字节 → 匹配第一个函数。
  • std::tuple<int,double,int>→ 大于 16 字节,可能匹配第二个函数(如果在 17~32 范围内)。
  • char→ 1 字节 → 匹配默认auto&

总结

  1. 概念参数设计
    • 第一个参数必须是类型。
    • 后续参数可以是常量或非类型参数,用于编译期条件约束。
  2. 使用场景
    • 限制类型大小
    • 限制数组长度
    • 限制枚举值
  3. 函数模板匹配规则
    • C++20 会根据概念约束自动选择最匹配的模板。
    • 不满足任何概念的类型,会匹配普通auto

目标

实现一个概念TupleOf<SIZE>,使得函数可以根据元组的元素数量(Num_Elements)进行重载:

voidfoo(constTupleOf<2>auto&);// 元素数量为2voidfoo(constTupleOf<3>auto&);// 元素数量为3

要求:

  • 可以匹配std::tuplestd::pair(因为pair可视为 2 元素元组)。
  • 对于非元组类型,匹配普通auto

核心思路

  1. 定义基本概念 Tuple
    • 检查类型是否有std::get<0>(t)可以访问。
    • 对空元组特殊处理。
template<classT>conceptEmptyTuple=std::same_as<T,decltype(std::tuple())>;// 空元组template<classT>conceptTuple=EmptyTuple<T>||requires(T t){std::get<0>(t);// 至少可通过 std::get<0> 访问第一个元素};
  1. 定义 TupleOf 概念
    • 在 Tuple 基础上增加元素数量约束:
template<classT,size_t SIZE>conceptTupleOf=Tuple<T>&&std::tuple_size_v<T>==SIZE;
  • std::tuple_size_v<T>在编译期得到元组或 pair 的元素数量。
  • 使用Tuple<T>保证类型确实是一个元组或类似元组的类型(支持std::get<>)。

函数模板重载

voidfoo(constTupleOf<2>auto&t){cout<<"TupleOf<2>: "<<std::get<0>(t)<<' '<<std::get<1>(t)<<'\n';}voidfoo(constTupleOf<3>auto&t){cout<<"TupleOf<3>: "<<std::get<0>(t)<<' '<<std::get<1>(t)<<' '<<std::get<2>(t)<<'\n';}voidfoo(constEmptyTupleauto&){cout<<"empty tuple\n";}// 所有非元组类型template<typenameT>requires(!Tuple<T>)voidfoo(constT&){cout<<"const auto&\n";}

匹配规则

  1. TupleOf<2> auto&→ 匹配元素数量为 2 的元组或 pair。
  2. TupleOf<3> auto&→ 匹配元素数量为 3 的元组。
  3. EmptyTuple auto&→ 匹配空元组。
  4. 其它类型→ 匹配普通模板函数。

main 测试

intmain(){std::tuple t2{1,"two"};// 2 元素 → TupleOf<2>foo(t2);// 输出 TupleOf<2>: 1 twostd::tuple t3{1,"two",3};// 3 元素 → TupleOf<3>foo(t3);// 输出 TupleOf<3>: 1 two 3std::pair p1{1,"two"};// pair → 2 元素 → TupleOf<2>foo(p1);// 输出 TupleOf<2>: 1 twofoo(7);// 非元组 → 输出 const auto&foo(std::tuple());// 空元组 → 输出 empty tuple}

核心特性总结

  1. 概念可以有非类型参数
    • TupleOf<SIZE>SIZE是非类型模板参数。
    • 在编译期即可对元组长度进行约束。
  2. 编译期重载选择
    • 编译器根据概念判断匹配函数模板。
    • 优先选择最精确匹配(例如TupleOf<2>优于普通auto)。
  3. 灵活性
    • 可以扩展到任意元素数量。
    • 支持std::tuplestd::pair以及空元组。

目标

实现一个函数foo,要求:

  1. 可以修改映射(map)中的值
  2. 不能修改映射本身(例如不能插入或删除键)。
  3. 支持std::mapstd::unordered_map
  4. 使用C++20 concepts限制类型。

核心问题

  • 普通 map 的operator[]会在键不存在时插入元素,这会破坏“不修改 map 结构”的要求。
  • 因此只能使用find+iterator->second来修改已有的值。

Concept 定义

template<typenameT>conceptConstDictMutableVals=requires(T t,typenameT::key_type key){typenameT::iterator;// 必须有迭代器类型typenameT::key_type;// 必须有 key_typetypenameT::mapped_type;// 必须有 mapped_type// t.find(key) 返回 T::iterator{t.find(key)}->std::same_as<typenameT::iterator>;// t.find(key) 可以和 t.end() 比较t.find(key)!=t.end();};

解释:

  1. 类型约束
    • T必须有iteratorkey_typemapped_type类型。
  2. 操作约束
    • t.find(key)必须返回迭代器。
    • 可以与t.end()比较,确保可以安全判断键是否存在。
  3. 不允许使用operator[]
    • 避免修改 map 结构,只允许通过迭代器修改值。

函数实现

voidfoo(ConstDictMutableValsauto&d){autoitr=d.find(1);// 查找 key = 1if(itr!=d.end())// 如果存在itr->second=3;// 修改值}
  • 这里仅修改已有键的值。
  • 使用find而非operator[]保证 map 结构不被修改。

测试

intmain(){map<int,int>myMap1={{1,0},{2,0}};foo(myMap1);// 修改 key=1 的值for(constauto&[k,v]:myMap1)std::cout<<k<<": "<<v<<std::endl;// 输出:// 1: 3// 2: 0unordered_map<int,int>myMap2={{1,0},{2,0}};foo(myMap2);// 同样生效for(constauto&[k,v]:myMap2)std::cout<<k<<": "<<v<<std::endl;// 输出:// 1: 3// 2: 0}

核心理解

  1. 概念约束了类型和操作
    • 使用requires确保传入类型具备必要成员和操作。
    • 这是一种精细化类型接口约束,类似静态接口(Static Interface)。
  2. 可修改值,不可修改容器结构
    • 通过限制不使用operator[],只允许find+iterator->second
  3. 兼容性
    • 支持std::mapstd::unordered_map等标准关联容器。
  4. 安全与编译期检查
    • 如果传入不符合ConstDictMutableVals的类型,编译期报错。
    • 提前避免运行时错误。
      小结
  • 关键点:用requires表达“允许的操作”而不是暴露整个容器。
  • 概念约束帮助编译器在编译期检查类型的接口。
  • 这种模式适合最小权限接口设计:只暴露函数需要的最小操作。

1⃣ 问题背景

原始做法:

Expression*e=newSum(newExp(newNumber(3),newNumber(2)),newNumber(-1));cout<<*e<<" = "<<e->eval()<<endl;deletee;

问题:

  1. 手动管理内存
    • new+delete不对称,容易内存泄漏。
  2. 表达式构造笨重
    • 嵌套new太长,阅读困难。
  3. 用户暴露实现细节
    • 用户必须知道底层用裸指针,增加认知负担。

2⃣ 使用智能指针

改为:

autoe=std::make_unique<Sum>(std::make_unique<Exp>(std::make_unique<Number>(3),std::make_unique<Number>(2)),std::make_unique<Number>(-1));cout<<*e<<" = "<<e->eval()<<endl;

改进:

  1. 内存自动管理,无需手动delete
  2. 避免裸指针泄漏。

问题仍然存在:

  • 语法仍然复杂,嵌套make_unique太长。
  • 用户仍需要关心智能指针细节(类型、移动语义)。

3⃣ 隐藏智能指针

目标代码:

autoe=Sum(Exp(Number(3),Number(2)),Number(-1));cout<<e<<" = "<<e.eval()<<endl;

改进点:

  1. 用户完全不关心智能指针。
  2. 内部实现可自由选择存储策略(unique_ptr或其他)。
  3. 构造语法简洁明了,接近数学表达式。

4⃣ 实现策略

template<charOp>classBinaryExpression:publicExpression{unique_ptr<Expression>e1,e2;// 私有实现,隐藏细节virtualdoubleevalImpl(doubled1,doubled2)const=0;public:template<typenameExpression1,typenameExpression2>BinaryExpression(Expression1 e1,Expression2 e2):e1(make_unique<Expression1>(std::move(e1))),e2(make_unique<Expression2>(std::move(e2))){}voidprint(ostream&out)constoverride{out<<'('<<*e1<<' '<<Op<<' '<<*e2<<')';}doubleeval()constoverride{returnevalImpl(e1->eval(),e2->eval());}};

核心设计思路:

  1. 私有智能指针成员
    • 用户不可访问,隐藏了内存管理细节。
  2. 模板构造函数
    • 可以接受任意Expression类型(如Number,Sum,Exp)。
    • 内部自动转换为unique_ptr
  3. 多态 eval
    • 调用evalImpl实现不同运算。
    • 保持接口统一。

5⃣ 具体操作实现

structSum:publicBinaryExpression<'+'>{usingBinaryExpression<'+'>::BinaryExpression;doubleevalImpl(doubled1,doubled2)constoverride{returnd1+d2;}};structExp:publicBinaryExpression<'^'>{usingBinaryExpression<'^'>::BinaryExpression;doubleevalImpl(doubled1,doubled2)constoverride{returnstd::pow(d1,d2);}};

特点:

  • using BinaryExpression<Op>::BinaryExpression
    继承模板构造函数,让派生类直接使用模板构造器。
  • evalImpl实现具体运算逻辑。
  • 用户完全不用关心指针,语法简洁。

6⃣ 使用示例

autoe=Sum(Exp(Number(3),Number(2)),Number(-1));cout<<e<<" = "<<e.eval()<<endl;// 输出: ((3 ^ 2) + (-1)) = 8

优点总结:

  1. 隐藏实现细节
    • 用户不需要知道内部使用了unique_ptr
  2. 易于维护和修改
    • 将来可以换成shared_ptr或其他存储方式,无需修改用户代码。
  3. 语法清晰
    • 接近数学表达式书写方式。
  4. 内存安全
    • 自动管理,无需delete

7⃣ 限制与注意事项

  • 目前不能直接传递已有对象的左值,例如:
    autoe2=Sum(e,Number(3));// 编译失败
    因为模板构造函数使用了std::move,要求传入右值。
  • 可以进一步改进,添加拷贝/移动构造支持,使左值也可传递。
    小结
  • 本设计思想体现了“最小接口暴露 + 隐藏实现细节”的理念。
  • 利用了模板 +unique_ptr+ 多态实现安全、简洁、可扩展的表达式树

1⃣ 隐藏继承层次细节(Hiding Inheritance Hierarchies Details)

使用的设计模式

  1. State Pattern(状态模式)
    • 核心思想:对象行为根据其内部状态变化。
    • 示例:一个Employee无论是FullTimeEmployeePartTimeEmployeeContractor,对外都是Employee类型,具体行为由内部状态决定。
  2. Strategy Pattern(策略模式)
    • 核心思想:算法或行为可以动态切换。
    • 示例:PathFinder可使用 BFS 或 DFS 算法,外部调用者只关心PathFinder,不必关心使用哪种算法。
  3. Factory Method(工厂方法)
    • 核心思想:封装对象创建过程,让调用者不需要知道具体类型。
    • 用户只得到基类或接口类型即可。
      总结:通过这些设计模式,可以让外部使用者不关心继承体系的复杂性,只依赖公共接口。

2⃣ 减少返回值信息量(Conveying less information on return values)

示例 1:直接返回具体类型

vector<int>foo1(){returnvector{1,2,4};}
  • 优点
    • 明确返回类型。
  • 缺点
    • 对调用者暴露了内部实现细节:必须是vector<int>
    • 限制了将来可能替换成其他容器(如std::array<int,3>std::deque<int>)。

示例 2:使用auto返回

autofoo2(){returnvector{1,2,4};}
  • 优点
    • 隐藏了具体类型,调用者不必关心返回容器类型。
    • 以后可以修改返回类型而不破坏接口。
  • 缺点
    • 对于使用者,无法静态限制返回类型特性(例如随机访问)。

示例 3:使用概念约束返回类型

template<typenameT,typenameElementType>conceptrandom_access_range_of=std::ranges::random_access_range<T>&&std::same_as<std::ranges::range_value_t<T>,ElementType>;random_access_range_of<int>autofoo3(){returnvector{1,2,4};}
  • 优点
    1. 隐藏具体类型,调用者不关心容器是vector还是array
    2. 保留必要特性:保证返回类型是随机访问范围(random_access_range) 并且元素类型是int
    3. 接口清晰:使用概念表达“我需要什么性质的返回值”,而不是具体类型。
  • 缺点
    • 需要 C++20 概念支持。
    • 对于简单函数可能略显复杂。

3⃣ 总结对比


方法隐藏信息类型限制灵活性
vector<int>暴露具体类型明确不灵活,容器固定
auto隐藏类型无法限制特性灵活,可更换容器
concept auto隐藏类型保证性质(如随机访问)灵活且安全

核心思想

  • 使用auto + 概念可以在隐藏实现细节的同时保留静态约束,兼顾安全与灵活性。
  • 适合公共接口设计,尤其在库开发中。

1⃣ 使用概念隐藏返回值类型(Conveying less information on return values)

核心点

  1. 概念作为返回值类型
    • C++20 支持:
      random_access_range_of<int>autofoo(){...}
    • 自动类型推导auto必须在函数体内实现(header 中)。
    • 编译器不会检查使用者的操作是否完全符合概念限制,只保证返回值在概念约束下可用。
  2. 接口隐藏与 Hyrum’s Law
    • 如果暴露太多实现细节,调用者可能依赖这些细节,造成leaky abstraction
    • Hyrum’s Law: “任何你允许的使用者行为,都会被依赖”,意味着即便你只想隐藏内部实现,用户也可能依赖暴露的细节,导致维护难度增加。
  3. 实现隐藏的方式
    • 概念返回类型(C++20)
      • 优点:类型隐藏,仍可表达静态约束。
      • 缺点:需要头文件内实现,编译器对使用者行为不做检查。
    • 接口 + 虚函数
      • 动态派发隐藏具体类型。
      • 缺点:引入运行时开销。
    • 包装类(Wrapper Class)
      • 可封装复杂类型或行为。
      • 优点:更灵活,仍可隐藏实现细节。
      • 缺点:工作量稍大。
  4. 注意“泄漏的抽象”
    • 典型例子:std::vector<bool>返回的 proxy 引用。
    • 用户可能依赖返回 proxy 的行为,而不是仅仅使用 bool,破坏了抽象封装。

2⃣ 隐藏辅助函数(Hiding Helper Functions)

问题示例

classThingy{public:voidfoo();// 注意:不要在调用 foo 之前调用 bar!voidbar();};
  • foobar有调用顺序依赖。
  • 用户可以错误地直接调用bar,导致使用错误。

尝试方案 1:组合公共函数

classThingy{voidfoo();voidbar();public:voidfoobar(){foo();bar();}};
  • 优点:用户只能调用foobar(),内部顺序固定。
  • 缺点:如果想动态条件执行bar,没有灵活性。

尝试方案 2:模板函数封装

classThingy{voidfoo();voidbar();public:template<typenameFunc>voidfoobar(Func&&func){foo();if(func())bar();}};
  • 优点:
    1. 用户只能调用foobar(),顺序安全。
    2. 可以传入 lambda 决定是否调用bar(),增加灵活性。
  • 结合示例:
#include<iostream>classThingy{voidfoo(){std::cout<<"foo"<<std::endl;}voidbar(){std::cout<<"bar"<<std::endl;}public:template<typenameFunc>voidfoobar(Func&&func){foo();if(func())bar();}};intmain(){Thingy t;t.foobar([]{returntrue;});}
  • 输出:
foo bar
  • 内部实现细节被完全隐藏,调用者不能误用foobar

总结

  1. 隐藏返回值类型
    • 用概念、接口或包装类隐藏实现,避免泄露具体类型。
    • 注意 Hyrum’s Law:用户可能依赖暴露的行为。
  2. 隐藏辅助函数
    • 将多个相关函数封装到单个公共接口中。
    • 可使用模板 + 回调函数实现灵活控制,同时保证调用顺序安全。
    • 典型模式:命令封装 + 回调
  3. 设计理念
    • 封装内部实现,让接口保持简单。
    • 避免用户依赖内部实现细节,减少维护风险。
    • 灵活性与安全性的平衡是关键。

1⃣ 核心思想

这个方案通过返回一个临时对象(临时类型BarCallable)来隐藏辅助函数的调用顺序和访问限制。

classThingy{voidinner_foo(){std::cout<<"foo"<<std::endl;}voidbar(){std::cout<<"bar"<<std::endl;}public:autofoo(){inner_foo();// 内部先执行 foo 的逻辑structBarCallable{Thingy*t;voidbar(){t->bar();}// 只能通过临时对象调用 bar};returnBarCallable(this);}};
  • inner_foo()是私有函数,用户不能直接调用。
  • bar()也是私有函数,用户不能直接调用。
  • foo()是公共接口,调用后返回一个局部 struct 类型对象BarCallable
  • 用户想调用bar()时,只能通过foo()返回的BarCallable对象:
t.foo().bar();// 调用顺序被强制:必须先调用 foo()
  • 如果用户只是调用t.foo();,只执行inner_foo()bar()不会被调用。

2⃣ 特性和优点

  1. 强制调用顺序
    • 用户不能直接调用bar()
    • bar()只能通过foo()返回的对象调用,保证了bar()前必先执行inner_foo()
  2. 隐藏内部实现
    • inner_foo()bar()都是私有的,实现细节不暴露给用户。
  3. 可链式调用
    • 通过返回临时对象,可写成:
      t.foo().bar();
  4. 灵活性
    • 用户可以选择只调用foo()
      t.foo();// 只执行 inner_foo()
    • 或者调用bar()
      t.foo().bar();// 执行 inner_foo() 后执行 bar()

3⃣ 与前面模板回调方法对比

特性模板回调方法临时对象返回方法
隐藏顺序调用模板回调保证顺序返回对象调用保证顺序
灵活性回调可控制是否执行 bar可选择是否调用返回对象的 bar
编译时类型依赖编译时确定是否调用 bar编译时确定 BarCallable 类型
代码可读性中等高,可链式调用,接口清晰
私有函数保护内部函数不暴露内部函数不暴露

4⃣ 使用示例

intmain(){Thingy t;t.foo().bar();// 输出: foo bart.foo();// 输出: foo}
  • 输出顺序:
foo bar foo
  • 符合预期:
    • bar()只能在foo()执行后被调用。
    • 用户无法错误地单独调用bar()inner_foo()

5⃣ 总结

这种“返回临时对象控制访问”的方法是一种安全封装辅助函数的高级技巧:

  1. 让内部实现细节保持私有。
  2. 强制执行调用顺序。
  3. 支持链式调用和可选调用。
  4. 接口对用户更直观、易用,同时内部逻辑安全。
    可以看作是“临时代理对象模式”(Temporary Proxy Object Pattern),在 C++ 中非常适合隐藏实现和约束调用顺序。

1⃣ 使用protected的原则与技巧

1.1 数据成员尽量私有

classParent{Wallet wallet;// 私有protected:shortneed_money(longdouble,conststd::string&reason);// 受保护};
  • 原则:数据成员应尽量声明为private,避免子类直接访问。
  • 受保护成员函数(protected)可被子类调用,但不对外公开。
  • 对比:
    classParent{protected:Wallet wallet;// 不推荐};
    这种方式会暴露内部状态给子类,违反封装原则。

1.2 调用示例

classChild:publicParent{public:voidendless_celebration(longdoubleamount){need_money(amount,"celebration");// 安全访问受保护方法}};
  • 子类通过protected方法访问内部状态或逻辑,而不直接暴露数据成员。

2⃣ 测试私有行为的建议

2.1 尽量避免直接测试私有数据

  • 私有状态(如类在状态 A)通常不应直接测试。
  • 如果必须测试:
    • 不要将成员改为public
    • 可以添加公共验证方法,用于状态检查或日志跟踪。

2.2 不通过测试修改私有状态

  • 测试中直接修改私有数据通常是不好的:
    • 不测试实际流程。
    • 可能导致测试不稳定或不真实。
  • 如果测试难以实现,可能说明代码耦合过高,需要重构或设计更好的解耦。

2.3 可行方法

  • 使用模拟(mocking)或构造错误场景,通过正常接口触发状态变化,而不是直接修改私有数据。
  • 如果仍需访问私有成员,可以通过友元测试特定访问接口,而不是直接暴露数据。

3⃣ 使用命名空间和上下文隐藏实现

  • 利用命名空间嵌套类型隐藏类型和实现细节。
  • 私有嵌套类型可用于内部代理对象。
  • C++20 模块提供更强的隐藏能力:
    • 可以隐藏模板实现。
    • 避免过多暴露内部类型。

4⃣ Private Token Idiom(私有令牌模式)

4.1 问题

  • 想让构造函数私有化,但仍希望使用make_unique创建对象:
    classFoo{public:Foo(intvalue):value(value){}// 想私有化staticstd::unique_ptr<Foo>create(intvalue){returnstd::make_unique<Foo>(value);}};
  • 直接私有化构造函数会导致make_unique编译失败。

4.2 解决方案:私有令牌模式

  • 定义一个私有嵌套类型PrivateToken
    classFoo{intvalue;classPrivateToken{};// 私有令牌public:Foo(intvalue,PrivateToken):value(value){}// 仅接受令牌构造staticstd::unique_ptr<Foo>create(intvalue){returnstd::make_unique<Foo>(value,PrivateToken{});// 使用令牌创建}intgetValue()const{returnvalue;}};
  • 用户不能直接构造Foo
    // Foo f(7); // 编译错误
  • 必须通过create工厂函数创建:
    autofoo=Foo::create(7);// 安全创建

4.3 优点

  1. 隐藏实现细节
    • 构造函数仅可通过私有令牌访问。
  2. 强制工厂函数使用
    • 确保对象创建逻辑一致。
  3. 避免接口泄露
    • 用户看不到内部实现或构造方式。

5⃣ 总结

  • protected
    • 数据成员尽量private
    • 函数可适当protected提供给子类。
  • 测试私有行为
    • 避免直接测试私有数据。
    • 使用公共接口或模拟。
  • 命名空间与模块
    • 提供上下文,隐藏内部实现。
  • Private Token Idiom
    • 工厂函数模式 + 私有令牌。
    • 隐藏构造函数实现,强制安全对象创建。
      这个模式非常适合:
  • 想隐藏构造逻辑或敏感接口
  • 强制使用工厂函数创建对象
  • 保持类的封装性与安全性

1⃣ 隐藏实现细节的重要性

  • 核心思想:隐藏实现细节不仅仅是设计建议,更是提高代码健壮性可维护性的重要手段。
  • 好的隐藏可以:
    • 降低外部对内部实现的依赖。
    • 内部修改不会影响外部代码。
    • 提高调试和测试的便利性。

2⃣ 隐藏规则与设计原则的关系

2.1 封装 (Encapsulation)

  • 保护对象完整性,只暴露必要接口。
  • 内部变化不影响外部使用。
  • 示例:
    • 数据成员尽量private
    • 提供protected或公共函数来访问内部逻辑,而不是暴露内部数据。

2.2 解耦 (Decoupling)

  • 组件只依赖于必要接口。
  • 系统各部分修改不会传导影响。
  • 组件更通用,测试更容易。
  • 使用概念、接口、工厂函数、私有令牌模式等都属于解耦策略。

3⃣ 私有字段并不够

  • 私有字段是必要条件,但不是充分条件
  • 为什么?
    • 上下文决定了哪些内容应该被公开。
    • 如果不需要,就不提供。
    • 一旦提供,用户可能会滥用。
  • 原则
    Trust no one! \text{Trust no one!}Trust no one!
    不假设用户会按规矩使用接口。

4⃣ 保持抽象 (Keep Abstractions)

  • 抽象 ≠ 模糊。
  • 抽象的目的是创建新的语义层次,在更高层次上精确表达概念。
  • 引用 Dijkstra 的观点:

    The purpose of abstraction is not to be vague, but to create a new semantic level in which one can be absolutely precise.

  • 应用
    • 使用接口、概念或工厂函数隐藏实现。
    • 使用私有类型、嵌套类、模块化等手段控制可见性。

5⃣ 避免错误理由暴露内部实现

  • 有些开发者会为了“方便”而公开内部实现,但大多数理由都是错误的。
  • 正确做法
    • 仔细判断哪些内容不应该暴露。
    • 保持接口最小化和精确。
    • 提前预防未来的缺陷和不必要的重构。

6⃣ 小结原则

  1. 封装优先:私有字段、受保护方法、公共接口。
  2. 解耦组件:只暴露必需的行为。
  3. 上下文敏感:不同使用场景可能需要不同可见性。
  4. 保持抽象:抽象提供精确语义,而不是模糊表达。
  5. 最小暴露:如果不需要,就不要公开。
  6. 工具支持:命名空间、嵌套类型、C++20 模块、概念、私有令牌模式等。

7⃣ 实际代码示例

私有令牌模式(Private Token Idiom)

classFoo{intvalue;classPrivateToken{};// 私有令牌Foo(intvalue,PrivateToken):value(value){}// 构造函数仅可由令牌调用public:staticstd::unique_ptr<Foo>create(intvalue){returnstd::make_unique<Foo>(value,PrivateToken{});}intgetValue()const{returnvalue;}};autofoo=Foo::create(7);// 安全创建
  • 隐藏构造函数实现。
  • 强制通过工厂函数创建对象。
  • 避免滥用和接口泄露。
    总结
    隐藏实现细节的目标是安全、可维护、可复用的代码。
  • 私有字段和保护成员只是基础。
  • 抽象、工厂、概念和模块是高级工具。
  • 保持接口最小化,暴露必要行为,才能有效控制复杂性。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/1 23:06:51

Qwen-Image-Edit-MeiTu:AI图像编辑的终极解决方案

Qwen-Image-Edit-MeiTu&#xff1a;AI图像编辑的终极解决方案 【免费下载链接】Qwen-Image-Edit-MeiTu 项目地址: https://ai.gitcode.com/hf_mirrors/valiantcat/Qwen-Image-Edit-MeiTu 还在为复杂的图像编辑软件头疼吗&#xff1f;Qwen-Image-Edit-MeiTu让每个人都能…

作者头像 李华
网站建设 2026/4/30 8:52:23

运放芯片tlv9051与lwv321参数对比

结合 TLV9051 的核心定位( 高精度、高速、低功耗 CMOS 运放),以下逐一拆解 11 个特性的 定义、通俗解读、实际应用价值,延续之前的 “参数 + 场景” 逻辑,同时对比 LMV321 突出其优势,帮你快速落地理解: 一、高速相关特性(压摆率 + 单位增益带宽)—— 决定 “处理快速…

作者头像 李华
网站建设 2026/5/2 17:00:57

从零实现3D Gaussian Splatting:完整渲染流程的PyTorch代码详解

3D Gaussian Splatting&#xff08;3DGS&#xff09;现在几乎成了3D视觉领域的标配技术。NVIDIA把它整合进COSMOS&#xff0c;Meta的新款AR眼镜可以直接在设备端跑3DGS做实时环境捕获和渲染。这技术已经不只是停留在论文阶段了&#xff0c;产品落地速度是相当快的。所以这篇文章…

作者头像 李华
网站建设 2026/4/28 3:24:43

springboot基于vue的大学生心理测试系统设计与实现_8o8lw7v5

目录已开发项目效果实现截图开发技术系统开发工具&#xff1a;核心代码参考示例1.建立用户稀疏矩阵&#xff0c;用于用户相似度计算【相似度矩阵】2.计算目标用户与其他用户的相似度系统测试总结源码文档获取/同行可拿货,招校园代理 &#xff1a;文章底部获取博主联系方式&…

作者头像 李华
网站建设 2026/5/1 16:12:46

AI 在数据库操作中的各类应用场景、方案与实践指南

概述随着人工智能技术的快速发展&#xff0c;AI 正在深刻改变数据库管理与操作的方式。从自动化查询生成到性能调优、数据质量监控&#xff0c;再到智能报表分析&#xff0c;AI 已成为现代数据库系统中不可或缺的“智能助手”。本文系统梳理了 AI 在数据库操作中的 8 大核心应用…

作者头像 李华
网站建设 2026/4/30 7:29:47

视觉色选机如何选?从多光谱到AI,核心技术揭秘

于食品加工跟农产品精选范畴之中&#xff0c;视觉色选机乃是达成自动化以及智能化分选的关键装备。它的工作原理是借助高分辨率相机去捕捉物料的光学特征&#xff0c;再结合光谱分析或者可见光成像&#xff0c;经由高速处理器与智能算法来实时识别异色粒、瑕疵品或者杂质&#…

作者头像 李华