cpp-ipc/MSVC_ISSUE_EXPLANATION.md
木头云 8bd5c83349 fix(container_allocator): Fix MSVC error by correcting allocator semantics
ROOT CAUSE:
The allocate() function was incorrectly constructing objects during memory
allocation, violating C++ allocator requirements. MSVC's std::_Tree_node has
a deleted default constructor, causing compilation failure.

CHANGES:
1. container_allocator::allocate() - Now only allocates raw memory without
   constructing objects (removed mem::$new and ipc::construct calls)
2. container_allocator::deallocate() - Now only frees memory without
   destroying objects (removed mem::$delete and ipc::destroy_n calls)

WHY THIS FIXES THE ISSUE:
- C++ allocator semantics require strict separation:
  * allocate()   -> raw memory allocation
  * construct()  -> object construction
  * destroy()    -> object destruction
  * deallocate() -> memory deallocation

- std::map and other containers call construct() with proper arguments
  (key, value) to initialize nodes, not allocate()

- std::_Tree_node in MSVC has no default constructor (= delete), so
  attempting to construct it without arguments always fails

- The previous code tried to default-construct objects in allocate(),
  which is both semantically wrong and impossible for _Tree_node

PREVIOUS FIX (uninitialized.h):
The earlier fix to uninitialized.h was insufficient - even with correct
T() vs T{} handling, you cannot default-construct a type with deleted
default constructor.

Fixes MSVC 2017 compilation error:
error C2280: attempting to reference a deleted function
2025-12-01 07:48:44 +00:00

362 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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<typename T>
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<value_type>(); // ❌ 这会构造对象!
} else {
void *p = mem::alloc(sizeof(value_type) * count);
for (std::size_t i = 0; i < count; ++i) {
// ❌ 在 allocate 中构造对象是错误的!
ipc::construct<value_type>(...);
}
return static_cast<pointer>(p);
}
}
```
**问题**
- `mem::$new<T>()` 会调用 `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<typename T>
struct _Tree_node {
T value;
_Tree_node* left;
_Tree_node* right;
// 没有默认构造函数!
_Tree_node() = delete;
// 只能通过数据来构造
template<typename... Args>
_Tree_node(Args&&... args) : value(std::forward<Args>(args)...) {}
};
```
## 详细解释
### 1. 标准容器如何使用 Allocator
```cpp
std::map<K, V, Cmp, Allocator> 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<int> 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<T>` 实现在某些情况下比 GCC 更严格。对于:
```cpp
std::_Tree_node<std::pair<const size_t, chunk_handle_ptr_t>, void*>
```
- **GCC**: `std::is_constructible<T>::value` = `true`
- **MSVC 2017**: `std::is_constructible<T>::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 <typename T, typename... A>
auto construct(void *p, A &&...args)
-> std::enable_if_t<!std::is_constructible<T, A...>::value, T *> {
return ::new (p) T{std::forward<A>(args)...}; // 使用 T{}
}
```
当调用 `construct<TreeNode>(ptr)` 时(零参数):
- **在 MSVC 上**
1. `std::is_constructible<TreeNode>` = `false`
2. SFINAE 选择第二个重载(`!is_constructible`
3. 尝试执行 `T{}`**编译错误!**
4. 因为 `_Tree_node` 不是聚合类型,也没有公开的默认构造函数匹配 `T{}`
- **在 GCC 上**
1. `std::is_constructible<TreeNode>` = `true`
2. SFINAE 选择第一个重载
3. 执行 `T()` → ✓ 成功
### 5. 为什么 `T()` 可以工作但 `T{}` 不行?
这是 C++ 的设计特性:
```cpp
struct Example {
std::unique_ptr<int> 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 <typename T>
T* construct(void *p) {
return ::new (p) T(); // 显式使用值初始化
}
template <typename T, typename A1, typename... A>
auto construct(void *p, A1 &&arg1, A &&...args)
-> std::enable_if_t<std::is_constructible<T, A1, A...>::value, T *> {
return ::new (p) T(std::forward<A1>(arg1), std::forward<A>(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<pointer>(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 <typename... P>
static void construct(pointer p, P && ... params) {
std::ignore = ipc::construct<T>(p, std::forward<P>(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()` 传递正确参数**