
您说: 继续讨论 more C++ idioms wikibook这本书,今天讨论一下这个惯用法:More C++ Idioms/Empty Base Optimization。这个惯用法当初主要意图是:Optimize storage for data members of empty class types
ChatGPT 说: Empty Base Optimization(EBO)惯用法 Empty Base Optimization (EBO) 是 C++ 中的一种优化技巧,主要用于减少对象的存储开销。当一个类派生自一个空基类时,编译器可以优化存储,使得这个空基类不会占据额外的空间。
1. 背景
在 C++ 语言中,即使一个类为空(即不包含任何成员变量),它仍然会占用至少 1 字节的空间,以保证不同对象在内存中的唯一性。例如:
class Empty {};
class NonEmpty {
Empty e;
int data;
};
在 NonEmpty 这个类中,Empty 作为成员变量,通常会占用至少 1 个字节,因此 NonEmpty 的大小不会只是 sizeof(int)。
但是,如果 Empty 作为基类,编译器可以利用 Empty Base Optimization (EBO) 进行优化,将空基类的存储空间合并,避免额外的字节开销。
2. Empty Base Optimization 示例
EBO 适用于多种情况,以下是一个简单示例:
#include <iostream>
class Empty {};
class Derived : public Empty {
int data;
};
int main() {
std::cout << "Size of Empty: " << sizeof(Empty) << "\n";
std::cout << "Size of Derived: " << sizeof(Derived) << "\n";
}
输出(可能因编译器不同而有所不同):
Size of Empty: 1
Size of Derived: 4
这里 Derived 类只包含一个 int 变量,尽管继承了 Empty,但 Empty 没有占用额外的空间。这就是 EBO 的优化作用。
3. 适用于 CRTP 模式
EBO 也被广泛用于 Curiously Recurring Template Pattern (CRTP),例如:
template <typename T>
class Base {};
class Derived : public Base<Derived> {
int data;
};
int main() {
std::cout << "Size of Derived: " << sizeof(Derived) << "\n";
}
由于 Base<T> 是空的,Derived 继承它时不会增加额外的存储开销。
4. 在标准库中的应用
std::pair 和 std::tuple 在 C++ 标准库中,std::pair 和 std::tuple 也利用 EBO 来优化存储。例如:
#include <iostream>
#include <utility>
class Empty {};
int main() {
std::pair<int, Empty> p;
std::cout << "Size of std::pair<int, Empty>: " << sizeof(p) << "\n";
}
在启用了 EBO 的编译器中,std::pair<int, Empty> 的大小仅为 sizeof(int),因为 Empty 作为基类不会占据额外空间。
5. 限制
EBO 不是所有情况下都适用,主要有以下限制:
多重继承可能会破坏 EBO
class Empty {};
class Base1 : public Empty {};
class Base2 : public Empty {};
class Derived : public Base1, public Base2 {};
由于 Base1 和 Base2 都继承 Empty,编译器可能无法合并两个 Empty 基类的存储,因此可能会导致 Derived 额外占用空间。
带有虚函数的类不会进行 EBO
class EmptyWithVTable {
virtual void foo() {}
};
class Derived : public EmptyWithVTable {};
由于 EmptyWithVTable 含有虚函数,因此它会有一个 vtable 指针,导致无法进行 EBO。
6. 结论
- EBO 主要用于优化存储,使得继承空类不会额外占用空间。
- 现代 C++ 标准库(如 std::pair 和 std::tuple)利用 EBO 进行存储优化。
- EBO 不能应用于虚基类或多重继承中的重复空基类。
- 这个优化技巧在泛型编程、CRTP、标准库容器中都有很重要的作用!🚀
您说: 很好奇,大概什么应用场景下,会用这个empty作为基类使用的场景?
ChatGPT 说: Empty Base Optimization(EBO)的实际应用场景 Empty Base Optimization(EBO)主要用于存储优化,尤其在泛型编程中非常重要。虽然 Empty 这个类看起来没有数据,但它可以用来提供接口、标记类型、类型萃取(Type Traits)等功能,而 EBO 允许这些功能类不占用额外空间。以下是一些常见的实际应用场景:
1. STL 容器的优化(std::pair, std::tuple)
核心思想:在 std::pair 和 std::tuple 里,如果某个元素是空类,EBO 可以减少内存占用。
示例:std::pair
#include <iostream>
#include <utility>
class Empty {};
int main() {
std::pair<int, Empty> p;
std::cout << "Size of std::pair<int, Empty>: " << sizeof(p) << "\n"; // 可能是 4,而不是 8
}
📌 作用:
std::pair 内部会继承 Empty 作为基类,从而节省 Empty 可能占据的 1 个字节。 如果 Empty 是成员变量,而不是基类,std::pair<int, Empty> 可能会是 8 字节(因为 Empty 至少占 1 字节)。 EBO 让它变成 4 字节(仅 int 的大小)。
2. CRTP(Curiously Recurring Template Pattern)
核心思想:让一个基类接收子类作为模板参数,通常用于 编译期多态,比如 std::enable_shared_from_this。
示例:EBO 在 CRTP 中减少存储开销
#include <iostream>
template <typename Derived>
class Base {}; // 空类,专门用于 CRTP
class Derived : public Base<Derived> {
int data;
};
int main() {
std::cout << "Size of Derived: " << sizeof(Derived) << "\n"; // 4,而不是 5
}
📌 作用:
Base
std::enable_shared_from_this<T>(智能指针的 shared_from_this 功能)。
boost::noncopyable(防止对象拷贝)。
type erasure(实现静态多态)。
3. 仿函数(Function Objects)
核心思想:有时,一个类仅仅是一个可调用对象(比如 operator()),但不需要额外的存储空间。
示例:EBO 优化 Lambda 或 Functor
#include <iostream>
struct EmptyFunctor {
void operator()() const {
std::cout << "Called EmptyFunctor\n";
}
};
template <typename Functor>
class Wrapper : public Functor {}; // 继承 Functor 以优化存储
int main() {
Wrapper<EmptyFunctor> w;
w(); // 调用 operator()
std::cout << "Size of Wrapper<EmptyFunctor>: " << sizeof(w) << "\n"; // 可能是 1 -> 0
}
📌 作用:
Wrapper
Lambda 表达式优化(如 std::function<>)。 策略模式(Strategy Pattern),避免占用不必要的空间。
4. 标记类(Tag Dispatching)
核心思想:有时候,我们需要一个“类型标记”,但它不需要占据存储空间。典型的做法是让它作为基类。
示例:EBO 用于编译期 Tag Dispatching
#include <iostream>
// 定义不同的策略类
struct FastMode {};
struct SafeMode {};
// 通过 EBO 让它们成为基类
template <typename Mode>
class Algorithm : public Mode {
public:
void run() {
if constexpr (std::is_base_of_v<FastMode, Mode>) {
std::cout << "Running in Fast Mode\n";
} else if constexpr (std::is_base_of_v<SafeMode, Mode>) {
std::cout << "Running in Safe Mode\n";
}
}
};
int main() {
Algorithm<FastMode> fast;
Algorithm<SafeMode> safe;
fast.run(); // Running in Fast Mode
safe.run(); // Running in Safe Mode
std::cout << "Size of Algorithm<FastMode>: " << sizeof(fast) << "\n"; // 1 -> 0
}
📌 作用:
FastMode 和 SafeMode 不占用存储空间,但可以在编译期决定行为。 这比 enum 或 bool 作为参数更高效,因为 enum 仍然需要存储 sizeof(int) 的空间。 🚀 应用案例:
std::allocator 用于不同的内存分配策略(比如 std::allocator_traits)。 算法策略模式(比如 std::sort 的不同策略)。
5. 继承 std::false_type / std::true_type
核心思想:C++ 类型萃取(Type Traits)经常使用 std::true_type 和 std::false_type,而它们本质上是空类,继承它可以触发 EBO,减少存储开销。
示例:EBO 用于 std::true_type / std::false_type
#include <iostream>
#include <type_traits>
// 定义不同的类型萃取
template <typename T>
struct IsPointer : std::false_type {};
template <typename T>
struct IsPointer<T*> : std::true_type {};
int main() {
std::cout << "IsPointer<int>: " << IsPointer<int>::value << "\n"; // 0
std::cout << "IsPointer<int*>: " << IsPointer<int*>::value << "\n"; // 1
}
📌 作用:
std::false_type 和 std::true_type 是空类,如果我们使用 EBO,可以在 IsPointer 继承时节省存储开销。 std::integral_constant、std::is_same 等很多 STL 类型萃取都用到了这个技巧。 🚀 应用案例:
SFINAE (enable_if) 用于编译期选择合适的模板特化。 STL 类型萃取 (std::is_pointer, std::is_integral)。
总结
EBO 在 C++ 泛型编程中有许多应用,常见的使用场景包括:
- STL 容器优化(std::pair, std::tuple)。
- CRTP 模式(如 std::enable_shared_from_this)。
- 仿函数与 Lambda 优化(std::function)。
- Tag Dispatching(标签派发)(如 std::allocator_traits)。
- 类型萃取与 std::true_type / std::false_type。
核心思想: 🔹 EBO 让空基类不占额外空间,这在泛型编程中极大地提高了存储和运行时效率。🚀
您说: 大概有点知道EBO什么意思,但这个似乎也不是从程序员角度谈的惯用法,只是编译器实现可能会实现EBO优化,我的问题是:编译器如何识别哪里是EBO适用的代码?
ChatGPT 说: 编译器如何识别 Empty Base Optimization (EBO) 适用的代码? 你的问题很有意思!EBO 确实更多是编译器实现层面的优化,而不是程序员主动使用的 C++ 习惯用法。但理解 编译器如何决定何时应用 EBO,对编写高效的 C++ 代码很有帮助。编译器主要通过静态分析和类型规则来判断是否可以安全地应用 EBO。
🛠 编译器应用 EBO 的核心规则
📌 规则 1:只有空类作为基类时,才可以应用 EBO
EBO 只能优化空基类,不影响空类作为成员变量时的存储布局。
示例:编译器应用 EBO
#include <iostream>
class Empty {}; // 空类
class A : public Empty {
int x;
};
int main() {
std::cout << "Size of A: " << sizeof(A) << "\n"; // 4,而不是 8
}
🔍 编译器分析:
Empty 是空类。 A 继承自 Empty 并且没有额外的虚函数或多继承,因此 Empty 不占额外空间。 结果:sizeof(A) == sizeof(int) == 4(假设 int 是 4 字节)。
📌 规则 2:如果空基类在不同继承层级多次出现,EBO 仍然有效
EBO 允许多个类共享相同的空基类,而不会重复占用存储空间。
示例:多个继承层级共享同一个空基类
#include <iostream>
class Empty {};
class Base1 : public Empty {};
class Base2 : public Empty {};
class Derived : public Base1, public Base2 {
int x;
};
int main() {
std::cout << "Size of Derived: " << sizeof(Derived) << "\n"; // 4,而不是 8
}
🔍 编译器分析:
Base1 和 Base2 都继承 Empty,但 Empty 仍然不会占用存储空间(因为它是空类)。 Derived 继承 Base1 和 Base2,最终 Empty 仍然不占空间。 结果:sizeof(Derived) == sizeof(int) == 4。 📌 重点:虽然 Empty 作为基类被继承了两次,但 EBO 让它仍然是零开销。
📌 规则 3:虚基类(virtual base)会破坏 EBO
如果一个空类被声明为 虚基类(virtual),那么编译器无法应用 EBO,因为虚基类在多继承下必须存储一个唯一的 vtable 指针。
示例:虚基类破坏 EBO
#include <iostream>
class Empty {};
class Base : virtual public Empty {
int x;
};
int main() {
std::cout << "Size of Base: " << sizeof(Base) << "\n"; // 8(假设指针大小是 4)
}
🔍 编译器分析:
Empty 被 Base 作为 虚基类 继承。 由于虚基类需要 额外的 vtable 指针 来确保唯一性,因此 Empty 不能再被优化掉。 结果:sizeof(Base) == 8(因为 x 是 int 4 字节,加上一个指向 Empty 的 vtable 指针 4 字节)。 📌 重点:如果一个空类被声明为 virtual base,EBO 不会生效,因为虚基类需要存储 vtable 指针。
📌 规则 4:非空类的成员变量会阻止 EBO
如果一个类包含非空数据成员,即使它是基类,编译器也不会应用 EBO。
示例:非空类成员破坏 EBO
#include <iostream>
class Empty {
int x; // 非空成员
};
class Derived : public Empty {
int y;
};
int main() {
std::cout << "Size of Derived: " << sizeof(Derived) << "\n"; // 8,而不是 4
}
🔍 编译器分析:
Empty 不是真正的空类,因为它有 int x 。 Derived 继承 Empty,因此必须存储 x 和 y。 结果:sizeof(Derived) == 8,无法应用 EBO。 📌 重点:EBO 只适用于真正的空类,如果类有成员变量,即使它是基类,EBO 也无效。
📌 规则 5:多重继承可能需要额外填充,影响 EBO
EBO 适用于单继承,但在多继承中,如果存在对齐要求,可能会导致填充字节(padding),从而影响 EBO。
示例:多重继承可能影响 EBO
#include <iostream>
class Empty {};
class Base1 : public Empty {};
class Base2 {
int x;
};
class Derived : public Base1, public Base2 {};
int main() {
std::cout << "Size of Derived: " << sizeof(Derived) << "\n"; // 4 或 8
}
🔍 编译器分析:
Empty 在 Base1 里被优化掉,但 Base2 有 int x 。 如果 Base2 需要 4 字节对齐,而 Base1 可能会被放置到 Base2 之前,会导致填充字节(padding)。 结果:有些编译器会使 sizeof(Derived) == 8,即使 EBO 本身仍然有效。 📌 重点:多继承可能会触发字节对齐填充,影响最终 sizeof 结果。
🔍 编译器如何实现 EBO? 不同的 C++ 编译器(如 GCC、Clang、MSVC)在实现 EBO 时略有不同,但它们的优化原则基本相同:
- GCC/Clang 默认启用 EBO,除非 -fno-empty-base 关闭优化。
- MSVC 在某些情况下不进行 EBO(如 /vd 选项影响 vtable 结构)。
- 编译器在 AST(抽象语法树)阶段分析类结构,确认哪些基类可以被优化。
总结
📌 编译器应用 EBO 的核心条件:
- 继承的基类必须是空类(没有成员变量,没有虚函数)。
- 不能是虚基类(virtual 关键字会禁用 EBO)。
- 不能有非空成员变量(即使是空类,含有数据成员会阻止优化)。
- 单继承时总是适用 EBO,但多继承可能因为对齐填充而无效。
💡 程序员角度的启示:
- 如果写泛型代码(如 std::pair, std::tuple),可以利用 EBO 来节省空间。
- 避免不必要的虚基类(virtual base)会影响优化。
- 如果 EBO 不生效,可能是由于字节对齐问题,可以手动调整类布局。
- EBO 是编译器的自动优化,但理解其工作方式,可以帮助我们编写更高效的 C++ 代码 🚀!
您说: 理解,那么在现代C++编译器里EBO优化是默认打开的吗,还是说需要额外的编译器选项开关控制?
ChatGPT 说: 现代 C++ 编译器中,EBO(Empty Base Optimization)默认启用吗? 在现代 C++ 编译器(如 GCC、Clang、MSVC)中,EBO 默认是启用的,但某些情况下可能会受到编译器选项、语言标准或特定代码模式的影响。下面我们具体分析不同编译器的行为,以及是否需要额外的编译选项来控制 EBO。
📌 GCC / Clang:默认启用 EBO 在 GCC 和 Clang 这类符合 C++ 标准的编译器中,EBO 默认是启用的,不需要任何额外的编译选项。
🔍 GCC / Clang 测试 EBO 你可以用以下代码测试:
#include <iostream>
class Empty {}; // 空类
class A : public Empty {
int x;
};
int main() {
std::cout << "Size of A: " << sizeof(A) << "\n"; // 预期输出 4
}
✅ 如果 sizeof(A) == 4,说明 EBO 被应用了(假设 int 为 4 字节)。
🚀 GCC 控制 EBO 的选项 在 GCC 中,你可以使用 -fno-empty-base 显式禁用 EBO:
g++ -fno-empty-base test.cpp -o test
-fno-empty-base:禁用 EBO。 默认行为:GCC 自动启用 EBO。 💡 结论:除非手动关闭,GCC/Clang 始终启用 EBO。
📌 MSVC(Microsoft Visual C++):默认支持,但受 /vd 选项影响 在 MSVC(Visual Studio 编译器)中,EBO 通常启用,但有些编译器选项会影响它的行为。
🔍 MSVC 测试 EBO 运行相同的 sizeof(A) 测试,如果输出 4,说明 MSVC 也应用了 EBO。
🚀 MSVC 控制 EBO 的选项 影响 EBO 的编译选项: /vd0:关闭虚表指针优化,可能影响 EBO。 /vd1(默认):启用虚表指针优化(推荐)。 /vd2:兼容某些旧代码模式,可能影响 EBO。 💡 结论:MSVC 也默认启用 EBO,但某些 /vd 选项可能会影响它。
📌 现代 C++ 语言标准对 EBO 的影响 C++ 标准本身没有强制规定 EBO,但 EBO 符合标准,因此:
C++98 / C++03:大多数编译器已经支持,但有时需要手动启用。 C++11 / C++14 / C++17 / C++20:EBO 默认启用,没有额外选项。 C++23:EBO 仍然适用,并且 std::tuple 等标准库容器利用了 EBO。 🔍 如何检查编译器是否应用了 EBO? 如果你想确认编译器是否应用了 EBO,可以使用 编译器的代码分析工具:
GCC / Clang:
g++ -fdump-tree-all test.cpp
这个选项会输出优化后的 AST(抽象语法树),你可以搜索 Empty 看它是否被去掉。 MSVC(Windows):
使用 Clang-cl 结合 /d1reportAllClassLayout: clang-cl /d1reportAllClassLayout test.cpp 使用 MSVC 直接查看类布局: cl /d1reportSingleClassLayoutA test.cpp 这会打印 A 类的内存布局,如果 Empty 没有额外占用存储,说明 EBO 生效。
🛠 结论
编译器 EBO 默认启用? 如何手动控制? GCC ✅ 默认启用 -fno-empty-base 关闭 Clang ✅ 默认启用 -fno-empty-base 关闭 MSVC ✅ 默认启用(受 /vd 影响) /vd0 可能会影响 EBO ✅ 重点总结
- EBO 在现代 C++ 编译器中默认启用,除非手动禁用(如 -fno-empty-base)
- MSVC 受 /vd 选项影响,但通常也会自动应用 EBO。
- 可以使用 sizeof(A) 测试是否应用了 EBO,或者用编译器的调试工具检查内存布局。
- 你可以试试在不同编译器上运行测试代码,看看 sizeof(A) 是否符合预期 😉!
您说: ||error: unrecognized command line option '-fno-empty-base'; did you mean '-Wno-empty-body'?|
ChatGPT 说: -fno-empty-base 选项在 GCC 版本 11 及以上 已经被移除,所以你看到的错误是因为这个选项已经 不存在 了。
在 现代 GCC(11+)和 Clang 中,EBO 默认是始终启用的,并且 没有选项可以禁用 EBO。
📌 GCC 和 Clang 在现代版本如何处理 EBO? GCC 10 及以下 可能支持 -fno-empty-base 选项来控制 EBO。 GCC 11 及以上(包括 Clang) 直接默认启用 EBO,并且移除了 -fno-empty-base,因为它几乎没有实际用途。 你可以用下面的代码来验证 EBO 是否生效:
#include <iostream>
class Empty {}; // 空类
class A : public Empty {
int x;
};
int main() {
std::cout << "Size of A: " << sizeof(A) << std::endl;
}
如果 sizeof(A) == 4(假设 int 为 4 字节),说明 EBO 默认启用。
📌 在 MSVC 上检查 EBO 如果你使用 Microsoft Visual C++(MSVC),可以运行:
cl /d1reportSingleClassLayoutA test.cpp
如果 Empty 没有额外的大小占用,说明 EBO 已经自动应用 了。
📌 结论 ✅ 现代 GCC/Clang:EBO 默认启用,无须手动控制,也无法禁用。 ✅ MSVC:EBO 也是默认启用的,但可以用 /d1reportSingleClassLayout 检查布局。 ❌ -fno-empty-base 已被移除,不要使用这个选项。
你可以直接运行 sizeof(A) 测试代码,EBO 仍然会生效!🎯

评论