diff --git a/MSVC_ISSUE_EXPLANATION.md b/MSVC_ISSUE_EXPLANATION.md new file mode 100644 index 0000000..da21698 --- /dev/null +++ b/MSVC_ISSUE_EXPLANATION.md @@ -0,0 +1,361 @@ +# VS 2017 编译错误深度分析(已更新) + +## 问题演进 + +### 第一次错误(已修复) +``` +error C2512: no appropriate default constructor available +note: Invalid aggregate initialization +``` + +### 第二次错误(真正的根本原因) +``` +error C2280: 'std::_Tree_node<...>::_Tree_node(void)': +attempting to reference a deleted function +``` + +## 真正的根本原因 + +**`container_allocator::allocate()` 函数违反了 C++ allocator 的基本语义!** + +### 错误的理解 + +最初我认为问题在于 `T()` vs `T{}` 的初始化差异,但这只是表象。 + +### 真正的问题 + +**`std::_Tree_node` 的默认构造函数在 MSVC 中被 `= delete` 删除了!** + +## C++ Allocator 的正确语义 + +### Allocator 的职责分离 + +C++ 标准要求 allocator 严格区分**内存分配**和**对象构造**: + +```cpp +// 正确的 allocator 接口 +template +class allocator { + // 1. allocate - 只分配原始内存,不构造对象 + T* allocate(size_t n); + + // 2. deallocate - 只释放内存,不销毁对象 + void deallocate(T* p, size_t n); + + // 3. construct - 在已分配的内存上构造对象 + void construct(T* p, Args&&... args); + + // 4. destroy - 销毁对象但不释放内存 + void destroy(T* p); +}; +``` + +### 原始代码的错误 + +```cpp +// 错误的实现 - allocate() 中构造了对象! +pointer allocate(size_type count) noexcept { + if (count == 1) { + return mem::$new(); // ❌ 这会构造对象! + } else { + void *p = mem::alloc(sizeof(value_type) * count); + for (std::size_t i = 0; i < count; ++i) { + // ❌ 在 allocate 中构造对象是错误的! + ipc::construct(...); + } + return static_cast(p); + } +} +``` + +**问题**: +- `mem::$new()` 会调用 `T` 的构造函数 +- `std::map` 的节点类型 `_Tree_node` 没有默认构造函数 +- MSVC 中 `_Tree_node()` 构造函数是 `= delete` 的 +- 即使改用 `T()`,仍然会尝试调用被删除的构造函数 + +### 为什么 `std::_Tree_node` 删除了默认构造函数? + +`std::map` 使用红黑树实现,节点类型的设计原则: + +1. **节点总是通过 allocator 的 `construct()` 构造**,而不是 `allocate()` +2. **节点必须包含具体的 key-value 数据**,不能"空"构造 +3. **删除默认构造函数防止误用** + +```cpp +// MSVC 的 std::_Tree_node 大致实现 +template +struct _Tree_node { + T value; + _Tree_node* left; + _Tree_node* right; + + // 没有默认构造函数! + _Tree_node() = delete; + + // 只能通过数据来构造 + template + _Tree_node(Args&&... args) : value(std::forward(args)...) {} +}; +``` + +## 详细解释 + +### 1. 标准容器如何使用 Allocator + +```cpp +std::map m; +// 当插入元素时: +// 1. 调用 allocator.allocate(1) - 分配内存 +// 2. 调用 allocator.construct(ptr, key, value) - 构造对象 +// 3. 使用完后: +// 4. 调用 allocator.destroy(ptr) - 销毁对象 +// 5. 调用 allocator.deallocate(ptr, 1) - 释放内存 +``` + +**关键**:`allocate()` 返回的是**未初始化的内存**,不能假设对象已构造! + +### 2. C++14 中的初始化类型(补充知识) + +#### 值初始化 (Value Initialization) - `T()` + +```cpp +T obj = T(); +new (ptr) T(); +``` + +**规则**: +- 如果 T 有**用户提供的**或**隐式生成的**默认构造函数 → 调用该构造函数 +- 如果 T 是聚合类型 → 所有成员被零初始化 +- 如果 T 是类类型且有隐式默认构造函数 → 调用该构造函数 + +#### 列表初始化 (List Initialization) - `T{}` + +```cpp +T obj = T{}; +new (ptr) T{}; +``` + +**规则(C++14)**: +- 如果 T 是**聚合类型** → 聚合初始化(直接初始化成员) +- 否则: + - 如果有匹配的 `std::initializer_list` 构造函数 → 优先调用 + - 否则,查找匹配的构造函数(包括默认构造函数) + - 如果没有匹配的构造函数 → **编译错误** + +### 2. 关键差异示例 + +```cpp +// 案例 1: 聚合类型 +struct Aggregate { + int x; + double y; +}; + +Aggregate a1 = Aggregate(); // ✓ 值初始化,x=0, y=0.0 +Aggregate a2 = Aggregate{}; // ✓ 聚合初始化,x=0, y=0.0 + +// 案例 2: 有隐式默认构造函数的类型 +struct ImplicitCtor { + std::unique_ptr ptr; // unique_ptr 有默认构造函数 + void* data; +}; + +ImplicitCtor b1 = ImplicitCtor(); // ✓ 值初始化,调用隐式生成的默认构造函数 +ImplicitCtor b2 = ImplicitCtor{}; // ✓ 也调用隐式默认构造函数 + +// 案例 3: 没有默认构造函数的非聚合类型 +struct NoDefaultCtor { + NoDefaultCtor(int x) {} // 只有带参构造函数 + // 隐式的默认构造函数被删除了 +}; + +NoDefaultCtor c1 = NoDefaultCtor(); // ✗ 编译错误:没有默认构造函数 +NoDefaultCtor c2 = NoDefaultCtor{}; // ✗ 编译错误:没有默认构造函数 +NoDefaultCtor c3 = NoDefaultCtor(42); // ✓ 可以 +NoDefaultCtor c4 = NoDefaultCtor{42}; // ✓ 可以 +``` + +### 3. MSVC 2017 的特殊问题 + +#### 问题根源 + +MSVC 的 `std::is_constructible` 实现在某些情况下比 GCC 更严格。对于: + +```cpp +std::_Tree_node, void*> +``` + +- **GCC**: `std::is_constructible::value` = `true` +- **MSVC 2017**: `std::is_constructible::value` = `false` (在某些情况下) + +#### 为什么会有这个差异? + +1. **MSVC 的实现细节**: + - MSVC 的 `_Tree_node` 可能有**受保护的**或**条件编译的**默认构造函数 + - `std::is_constructible` 检查的是"**公开可访问的构造函数**" + - 但实际上,通过 placement new,编译器仍然可以调用隐式生成的构造函数 + +2. **C++ 标准的灰色地带**: + - C++14 标准对于 `std::is_constructible` 的具体实现有一定的解释空间 + - MSVC 的实现更保守,只有明确可访问的构造函数才返回 `true` + - GCC 的实现更宽松,只要类型理论上可构造就返回 `true` + +### 4. 原始代码的问题 + +```cpp +// 原始版本 +template +auto construct(void *p, A &&...args) + -> std::enable_if_t::value, T *> { + return ::new (p) T{std::forward(args)...}; // 使用 T{} +} +``` + +当调用 `construct(ptr)` 时(零参数): + +- **在 MSVC 上**: + 1. `std::is_constructible` = `false` + 2. SFINAE 选择第二个重载(`!is_constructible`) + 3. 尝试执行 `T{}` → **编译错误!** + 4. 因为 `_Tree_node` 不是聚合类型,也没有公开的默认构造函数匹配 `T{}` + +- **在 GCC 上**: + 1. `std::is_constructible` = `true` + 2. SFINAE 选择第一个重载 + 3. 执行 `T()` → ✓ 成功 + +### 5. 为什么 `T()` 可以工作但 `T{}` 不行? + +这是 C++ 的设计特性: + +```cpp +struct Example { + std::unique_ptr ptr; + void* data; + // 隐式生成的默认构造函数存在,但可能不满足 is_constructible 的检查 +}; + +void* mem = ::operator new(sizeof(Example)); + +// T() - 值初始化 +// 编译器会尝试调用任何可用的默认构造函数(包括隐式生成的) +Example* p1 = ::new (mem) Example(); // ✓ 总是成功(如果类型可默认构造) + +// T{} - 列表初始化 +// 需要找到明确匹配的构造函数或者是聚合类型 +Example* p2 = ::new (mem) Example{}; // ✓ 在这个例子中也成功 +``` + +但在 MSVC 的 `_Tree_node` 实现中: +- `T()` 能找到隐式的默认构造函数 +- `T{}` 找不到公开的匹配构造函数(因为不是聚合,且默认构造函数可能不满足条件) + +### 6. 修复方案的原理 + +```cpp +// 修复后的版本 +template +T* construct(void *p) { + return ::new (p) T(); // 显式使用值初始化 +} + +template +auto construct(void *p, A1 &&arg1, A &&...args) + -> std::enable_if_t::value, T *> { + return ::new (p) T(std::forward(arg1), std::forward(args)...); +} +``` + +**优势**: +1. **零参数情况**:直接使用 `T()`,避开了 `std::is_constructible` 的判断差异 +2. **有参数情况**:通过 SFINAE 正确分发到直接初始化或聚合初始化 +3. **跨编译器兼容**:不依赖编译器对 `is_constructible` 的具体实现 + +## 修复方案 + +### 正确的 `container_allocator` 实现 + +```cpp +// 修复后:allocate 只分配内存 +pointer allocate(size_type count) noexcept { + if (count == 0) return nullptr; + if (count > this->max_size()) return nullptr; + // ✅ 只分配原始内存,不构造对象 + void *p = mem::alloc(sizeof(value_type) * count); + return static_cast(p); +} + +// 修复后:deallocate 只释放内存 +void deallocate(pointer p, size_type count) noexcept { + if (count == 0) return; + if (count > this->max_size()) return; + // ✅ 只释放内存,不销毁对象(对象应该已经被 destroy() 销毁) + mem::free(p, sizeof(value_type) * count); +} + +// construct 和 destroy 保持不变 +template +static void construct(pointer p, P && ... params) { + std::ignore = ipc::construct(p, std::forward

(params)...); +} + +static void destroy(pointer p) { + std::ignore = ipc::destroy(p); +} +``` + +### 为什么这个修复能解决问题? + +1. **`allocate()` 不再尝试构造对象**: + - 只分配原始内存 + - 不会调用 `std::_Tree_node` 的构造函数 + - MSVC 不会报 "deleted function" 错误 + +2. **遵循 C++ allocator 标准**: + - `std::map` 会正确调用 `construct()` 来构造节点 + - 传递正确的参数(key, value) + - `_Tree_node(key, value)` 构造函数存在且可用 + +3. **跨编译器兼容**: + - 所有符合标准的编译器都期望这种行为 + - 不依赖编译器特定的实现细节 + +## 总结 + +### 回答你的问题 + +> VS 2017 应该能正常支持 C++14,为何一个不能通过 `T{}` 构造的类型可以通过 `T()` 来构造? + +**答案已更新**: + +1. **第一层问题**(表象): + - `T()` 和 `T{}` 确实有区别 + - 但这不是根本原因 + +2. **第二层问题**(根本): + - `std::_Tree_node` 在 MSVC 中**删除了默认构造函数** + - `T()` 和 `T{}` **都不能**用于默认构造它 + - 即使修复了 `uninitialized.h`,问题依然存在 + +3. **真正的根本原因**: + - **`allocate()` 不应该构造对象!** + - 违反了 C++ allocator 的基本设计原则 + - 容器(如 `std::map`)期望从 `allocate()` 得到未初始化的内存 + - 然后通过 `construct()` 用正确的参数构造对象 + +### 修改文件清单 + +1. ✅ `include/libipc/imp/uninitialized.h` - 分离零参数构造(虽然最终不需要) +2. ✅ `include/libipc/mem/container_allocator.h` - **修复 allocator 语义**(真正的修复) + +### 关键教训 + +- ❌ **不要在 `allocate()` 中构造对象** +- ✅ **严格遵守 allocator 的语义分离**: + - `allocate` = 分配内存 + - `construct` = 构造对象 + - `destroy` = 销毁对象 + - `deallocate` = 释放内存 +- ✅ **不要假设所有类型都有默认构造函数** +- ✅ **标准库容器会调用 `construct()` 传递正确参数** diff --git a/include/libipc/mem/container_allocator.h b/include/libipc/mem/container_allocator.h index 4087b00..97b8449 100644 --- a/include/libipc/mem/container_allocator.h +++ b/include/libipc/mem/container_allocator.h @@ -63,26 +63,18 @@ public: pointer allocate(size_type count) noexcept { if (count == 0) return nullptr; if (count > this->max_size()) return nullptr; - if (count == 1) { - return mem::$new(); - } else { - void *p = mem::alloc(sizeof(value_type) * count); - if (p == nullptr) return nullptr; - for (std::size_t i = 0; i < count; ++i) { - std::ignore = ipc::construct(static_cast(p) + sizeof(value_type) * i); - } - return static_cast(p); - } + // Allocate raw memory without constructing objects + // Construction should be done by construct() member function + void *p = mem::alloc(sizeof(value_type) * count); + return static_cast(p); } void deallocate(pointer p, size_type count) noexcept { if (count == 0) return; if (count > this->max_size()) return; - if (count == 1) { - mem::$delete(p); - } else { - mem::free(ipc::destroy_n(p, count), sizeof(value_type) * count); - } + // Deallocate raw memory without destroying objects + // Destruction should be done by destroy() member function before deallocate + mem::free(p, sizeof(value_type) * count); } template