mirror of
https://github.com/mutouyun/cpp-ipc.git
synced 2025-12-06 16:56:45 +08:00
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
This commit is contained in:
parent
d65eafe86a
commit
8bd5c83349
361
MSVC_ISSUE_EXPLANATION.md
Normal file
361
MSVC_ISSUE_EXPLANATION.md
Normal file
@ -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<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()` 传递正确参数**
|
||||
@ -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<value_type>();
|
||||
} 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<value_type>(static_cast<byte *>(p) + sizeof(value_type) * i);
|
||||
}
|
||||
return static_cast<pointer>(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<pointer>(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 <typename... P>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user