CppCon 2018 学习:Standard Library Compatibility Guidelines (SD-8)

发布于:2025-07-06 ⋅ 阅读:(16) ⋅ 点赞:(0)

Standard Library Compatibility Guidelines (SD-8)` 是 C++ 标准委员会(WG21)提出的技术文档之一,编号 SD-8,全称为:

SD-8: Compatibility of the C++ Standard Library

它主要定义了:
不同 C++ 版本之间,标准库的兼容性策略与设计指导原则

SD-8 的核心目标:

确保 C++ 标准库在不同版本之间兼容,即便新增功能,也不破坏已有代码的正确性和行为。

为什么需要 SD-8?

C++ 标准每隔几年就会更新一次(C++11、C++14、C++17、C++20、C++23…),标准库也在不断引入新特性。

SD-8 的职责是:

  • 指导库的 新增功能如何设计,避免破坏已有接口
  • 确保用户 升级编译器后,旧代码不会崩溃或行为变化
  • 定义 何时允许破坏性变更(breaking change)

SD-8 核心内容概览

1. 向后兼容性(Backward Compatibility)

  • 保证老版本代码可以在新版本中继续工作
  • 不允许更改已有函数的行为(除非 bug fix)
  • 例如:
    • 不应改动 std::vector::size() 的返回类型
    • 不应更改 std::string 的语义

2. 向前兼容性(Forward Compatibility)

  • 不保证未来标准中的代码能在旧编译器中工作
  • 即新特性不需要考虑旧编译器支持

3. ABI(Application Binary Interface) 稳定性

  • ABI 不应轻易更改
  • 如果标准库的类型布局(如 std::string)改变了,可能会导致旧程序无法链接
  • 标准库实现者需 谨慎维护 ABI 兼容

4. Feature Checking 与 分版本

  • 使用 #ifdef__cpp_xxx__cplusplus 检查新功能是否存在
  • 避免在代码中依赖某个具体的编译器实现细节

5. Deprecation(弃用策略)

  • 使用 [[deprecated]] 标签提示用户某些功能即将移除
  • 给出迁移建议
  • 真正移除要跨多个版本,并充分通告

举个例子:std::auto_ptr 被移除

  • auto_ptr 是 C++98 中的智能指针,后来被发现存在所有权问题
  • C++11 中引入了更安全的 unique_ptr,并将 auto_ptr 标记为 [[deprecated]]
  • 到了 C++17,auto_ptr 被彻底移除
  • 这就是 SD-8 指导下的一个“安全弃用过程”

小结表格

内容 是否保证
向后兼容
向前兼容
ABI 稳定性(尽量)
添加新功能不破坏旧接口
移除旧功能(需要弃用流程)

总结一句话:

SD-8 是为了在持续演进 C++ 标准库的同时,不破坏旧代码的行为和编译能力。

如果你是标准库开发者(如参与 libc++ 或 libstdc++),SD-8 是设计变更时必须遵循的底层兼容契约。

这段内容来自对 C++ 标准演进过程的一种反思与调侃风格的讲解,核心话题是:

“Upgrades: Cheap?” —— C++ 升级容易吗?

这是讲者在引导思考一个问题:

在你升级到新版本 C++ 时,过程是否“便宜”(轻松、安全、无痛)?

答案通常是:不一定

内容解读与翻译

“Upgrades: Cheap?”

升级很便宜吗?

  • 这是讽刺,实际上暗示:C++ 升级常常带来编译器警告、行为变化、甚至崩溃。
  • 原因通常是:新标准添加、删除、或重新定义了一些语言特性。

“Would you like it to be easier?”

你希望升级更容易吗?

  • 这是面向标准委员会或生态系统的建议:
    • 是否应该设计得更稳健,让升级不会轻易破坏已有代码?

“Stable Code?”

你的项目很稳定,所以你永远不会升级?

  • 这是在质疑一些团队的“保守策略”:
    • 项目稳定,就停在 C++11,不管后面的 C++17/20/23。
  • 讲者调侃说:

    如果你真不打算升级,那你来错房间了。

“History: Chaos”

回顾历史:混乱

举例几个疑似 未定义行为(UB) 的写法:

Foo(&std::move);         //  函数指针拿的是哪个std::move?(likely UB)
std::vector<int> v;
Foo(&v.size());          //  拿地址可能UB,取决于 Foo 的定义
namespace std {
  class MyClass { ... }; //  不能定义你自己的std::MyClass,这是未定义行为
}

重点:C++ 语言历史中很多行为的边界不清晰,很多用户以为“合法”的代码其实是 UB(未定义行为)

“The committee could do a better job”

讲者指出一个问题:

C++ 标准委员会应该更清楚地说明哪些行为是非法(例如,哪些是未定义行为),并且一旦说了,就要始终坚持。

“Recent: Some Hope”

最近:有点希望了

  • 表示从 C++17 起,委员会越来越重视兼容性标准库行为稳定性
  • 比如:
    • 明确了更多 UB 行为
    • 引入 [[deprecated]]constexpr、更安全的库接口等
    • 避免大刀阔斧移除东西,走向 SD-8 指导的路线

总结:

内容 解读
Upgrades: Cheap? 升级代价并不小(编译错误、UB、行为变)
History: Chaos C++ 早期有许多灰色地带和危险行为
The committee could do better C++ 标准委员会应更明确地定义边界
Recent: Some hope 新标准越来越重视稳定和兼容性

对开发者的建议:

  1. 小心升级新标准(如从 C++11 升级到 C++20)
  2. 保持关注标准演进(如 SD-8、SD-6 等)
  3. 编译时开最多的警告(-Wall -Wextra)
  4. 多使用现代工具(如 clang-tidy、ASan、UBSan)

讲解 C++ 标准库兼容性指导原则中的 SD-8,重点是:

SD-8: 标准库中允许“添加新名字”的规则和限制

1. 标准库保留的权利

C++ 标准允许:

  • std 命名空间 添加新的名字(类、函数、模板等)
  • 给现有的 std 类型添加新的成员函数
  • 给已有函数增加新的重载版本
  • 给函数和模板添加新的默认参数
  • 兼容地改变函数返回类型(比如 void 改为其他类型,或者数值类型宽化)
  • 兼容地修改接口,只要这些接口用法符合标准要求(实例化类型和调用函数)

关键点:

用户代码不应依赖这些实现细节,如类型的“主要名称”、函数的具体实现等。

2. 为什么要限制用户自定义std里的东西?

示例说明了如果用户直接往 std 里写代码:

namespace std {
  class optional { ... }; // 用户自己写的optional
}

或不小心通过 using namespace std; 引入后自己定义了类似名字:

using namespace std;
struct optional { ... }; // 用户定义的optional,和std的冲突

会导致:

  • 标准库想引入 std::optional 的时候,用户代码会冲突
  • 标准库新增函数、类时,可能和用户自定义名字碰撞,导致编译错误或行为异常

3. 标准委员会和库作者的做法

标准委员会说:“我们要添加新的名字到 std(比如 std::optionalstd::contains),用户不能在自己的代码里搞同名的!”

举例:

  • 用户在 libs 命名空间里写了 bool contains(std::string_view needle, std::string_view haystack) 函数
  • 标准库后面想加入 std::contains,就可能冲突

4. 额外说明:参数依赖查找 (ADL)

  • 调用未限定的函数名时,编译器会查找所有“相关命名空间”里的同名函数
  • 这会引起意想不到的函数调用或冲突,尤其是涉及标准类型时

5. 结论和建议

  • 不要在自己的代码中往 std 命名空间里添加东西!
  • 避免给标准库类型写未限定的同名函数(尤其是 snake_case 风格)
  • 尽量使用自己的命名空间,且不要引入 using namespace std; 到公共接口或全局作用域
  • 避免用户代码和标准库名字冲突,保持标准库接口兼容性和未来可升级性

总结

关键点 说明
标准库允许新增名字和重载 以保持向后兼容和扩展性
用户代码不得在 std 里写代码 避免和标准库新名字冲突
避免未限定调用标准类型相关函数 以防止 ADL 造成意外行为
使用自定义命名空间管理代码 防止污染 std,提高代码健壮性

这段内容依旧是关于 SD-8(C++标准库兼容性指导原则)的讲解,重点在标准库新增成员函数、重载、默认参数等扩展带来的影响,以及用户代码该如何避免冲突。下面帮你总结和解释:

SD-8 深入讲解(标准库兼容性相关)

1. 标准库有权利:

  • std 命名空间新增名字(函数、类、变量等)
  • 向已有 std 类型添加新成员函数
  • 给已有函数添加新的重载版本
  • 给函数和模板增加新的默认参数
  • 兼容地更改函数返回类型(如 void → 非 void,数值类型宽化等)
  • 兼容地修改接口,只要不破坏已有的调用和实例化

注意:用户代码不应依赖具体实现细节(类型名称、函数细节等)

2. 用户该避免什么?

  • 不要自己向 std 命名空间添加名字
    避免和标准库未来的新增名字冲突。
  • 不要写 using namespace std;,尤其在头文件和公共接口中,避免引入大量名字导致冲突。
  • 不要对标准类型进行未限定的“snake_case”函数调用,防止通过参数依赖查找(ADL)引起冲突。
  • 不要用“检测惯用法(detection idiom)”针对标准类型写元编程代码,因为标准库成员函数可能新增,写死检测会导致错误。

3. 关于“新增重载”

  • 标准库可能给已有成员函数(如 std::vector<int>::size)或函数(如 std::iswalpha)增加重载版本。
  • 用户自己写类似重载可能引发:
    • 编译错误(重载二义性)
      std::cout << v;  // 如果用户和库都定义了 operator<<,可能出现歧义。
      
    • One Definition Rule(ODR)违规
      如果用户和库都定义了相同函数,且定义不一致,程序行为未定义。

4. ODR(One Definition Rule)简述

  • 所有用到的东西必须至少定义一次
  • 某些东西必须且只能定义一次(变量、非内联函数)
  • 类、模板、内联函数可以定义多次,但必须“完全一致”
  • 定义不一致,程序行为未定义

5. 示例说明

用户写的:

std::ostream& operator<<(std::ostream& os, const std::vector<int>& v) {
  for (int i = 0; i < v.size() - 1; ++i)
    os << v[i] << ", ";
  os << v.back();
  return os;
}

标准库或其他库也写了类似:

std::ostream& operator<<(std::ostream& os, const std::vector<int>& v) {
  for (int i = 0; i < v.size() - 1; ++i)
    os << v[i] << " ";
  os << v.back();
  return os;
}
  • 编译可能报二义性错误
  • 如果强制链接多个定义,触发 ODR 违规,行为不确定

6. 推荐做法

  • 不要为标准类型重载操作符,尤其是流操作符(operator<<
  • 可以在自己的命名空间写专用打印函数,例如:
namespace mine {
  void Print(const std::vector<int>& v);
  void Print(const std::vector<int>& v, std::ostream& os);
}
  • 避免污染全局命名空间,减少与标准库冲突风险

7. 总结

重点 建议
标准库可扩展接口 用户不要试图复制或覆盖
避免对标准类型元编程检测 防止未来库升级引发代码错误
不在 std 中自定义名字 也避免 using namespace std;
不对标准类型重载操作符 改用命名空间内的自定义函数
ODR 违规风险 确保每个函数定义唯一且一致

SD-8(C++标准库兼容性指导原则) 更详细的补充,重点在于:

SD-8 核心内容扩展与理解

1. 标准库有权利继续扩展:
  • 新增名字(类、函数、变量等)到 std 命名空间。
  • 给已有类型增加新成员函数。
  • 给已有函数增加新重载。
  • 给函数和模板添加默认参数。
  • 在兼容的前提下更改函数的返回类型(比如从 void 变为其他,或者数值类型做宽化转换)。
  • 只要是兼容的,标准库可以改变已有接口的实现细节。
    关键提醒:用户代码不应依赖标准库的实现细节(比如类型的具体名称、函数的调用细节等)。
2. 不要做的事情:
  • 不要对标准库函数或成员函数取地址
    因为标准库可能给同名函数添加新的重载或默认参数,导致你取地址的函数指针变得不明确或者不匹配。
  • 不要对标准库类型进行特化或重新定义,比如:
    • iostream 流对象相关特化
    • std::swap 的特化
  • 不要自己前置声明标准库的类型或函数(避免声明与实际不符)。
3. 不要做元编程(模板元编程)时直接依赖标准库的具体实现
  • 比如不要写如下代码:
    void MySwap(int& a, int& b) {
      return std::swap(a, b);
    }
    
    因为 std::swap 未来可能会改返回类型或者添加重载,代码可能产生不兼容或歧义。
4. 不要依赖“复制/移动”的具体实现细节
  • 标准库可能改变对象的拷贝和移动次数或顺序。
  • 不要设计移动操作比拷贝更贵重的类型然后抱怨,这违反了设计预期。
5. 关于 Hyrum’s Law
  • 无论标准库或API如何保证兼容,总会有人依赖某些实现细节或未文档化行为
  • 因此,API设计者应谨慎处理接口变化,保持兼容性,但也要意识到“不可避免的破坏”。

总结

重点 解释/建议
标准库可增改接口 允许新增名字、成员、重载、默认参数、兼容改动
不依赖实现细节 用户不应依赖类型具体名称和函数实现细节
不对 std 类型做特化 避免破坏标准库或触发未定义行为
不取标准函数地址 可能因重载默认参数改变导致函数指针无效
不自己声明标准库类型 避免声明错误或冲突
不依赖复制/移动实现细节 标准库可能优化拷贝和移动次数,不保证顺序和次数
认识 Hyrum 法则 即使你保证接口稳定,仍可能有人依赖实现细节

网站公告

今日签到

点亮在社区的每一天
去签到