mirror of
https://github.com/mutouyun/cpp-ipc.git
synced 2025-12-06 08:46:45 +08:00
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
11 KiB
11 KiB
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 严格区分内存分配和对象构造:
// 正确的 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);
};
原始代码的错误
// 错误的实现 - 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 使用红黑树实现,节点类型的设计原则:
- 节点总是通过 allocator 的
construct()构造,而不是allocate() - 节点必须包含具体的 key-value 数据,不能"空"构造
- 删除默认构造函数防止误用
// 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
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()
T obj = T();
new (ptr) T();
规则:
- 如果 T 有用户提供的或隐式生成的默认构造函数 → 调用该构造函数
- 如果 T 是聚合类型 → 所有成员被零初始化
- 如果 T 是类类型且有隐式默认构造函数 → 调用该构造函数
列表初始化 (List Initialization) - T{}
T obj = T{};
new (ptr) T{};
规则(C++14):
- 如果 T 是聚合类型 → 聚合初始化(直接初始化成员)
- 否则:
- 如果有匹配的
std::initializer_list构造函数 → 优先调用 - 否则,查找匹配的构造函数(包括默认构造函数)
- 如果没有匹配的构造函数 → 编译错误
- 如果有匹配的
2. 关键差异示例
// 案例 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 更严格。对于:
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(在某些情况下)
为什么会有这个差异?
-
MSVC 的实现细节:
- MSVC 的
_Tree_node可能有受保护的或条件编译的默认构造函数 std::is_constructible检查的是"公开可访问的构造函数"- 但实际上,通过 placement new,编译器仍然可以调用隐式生成的构造函数
- MSVC 的
-
C++ 标准的灰色地带:
- C++14 标准对于
std::is_constructible的具体实现有一定的解释空间 - MSVC 的实现更保守,只有明确可访问的构造函数才返回
true - GCC 的实现更宽松,只要类型理论上可构造就返回
true
- C++14 标准对于
4. 原始代码的问题
// 原始版本
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 上:
std::is_constructible<TreeNode>=false- SFINAE 选择第二个重载(
!is_constructible) - 尝试执行
T{}→ 编译错误! - 因为
_Tree_node不是聚合类型,也没有公开的默认构造函数匹配T{}
-
在 GCC 上:
std::is_constructible<TreeNode>=true- SFINAE 选择第一个重载
- 执行
T()→ ✓ 成功
5. 为什么 T() 可以工作但 T{} 不行?
这是 C++ 的设计特性:
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. 修复方案的原理
// 修复后的版本
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)...);
}
优势:
- 零参数情况:直接使用
T(),避开了std::is_constructible的判断差异 - 有参数情况:通过 SFINAE 正确分发到直接初始化或聚合初始化
- 跨编译器兼容:不依赖编译器对
is_constructible的具体实现
修复方案
正确的 container_allocator 实现
// 修复后: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);
}
为什么这个修复能解决问题?
-
allocate()不再尝试构造对象:- 只分配原始内存
- 不会调用
std::_Tree_node的构造函数 - MSVC 不会报 "deleted function" 错误
-
遵循 C++ allocator 标准:
std::map会正确调用construct()来构造节点- 传递正确的参数(key, value)
_Tree_node(key, value)构造函数存在且可用
-
跨编译器兼容:
- 所有符合标准的编译器都期望这种行为
- 不依赖编译器特定的实现细节
总结
回答你的问题
VS 2017 应该能正常支持 C++14,为何一个不能通过
T{}构造的类型可以通过T()来构造?
答案已更新:
-
第一层问题(表象):
T()和T{}确实有区别- 但这不是根本原因
-
第二层问题(根本):
std::_Tree_node在 MSVC 中删除了默认构造函数T()和T{}都不能用于默认构造它- 即使修复了
uninitialized.h,问题依然存在
-
真正的根本原因:
allocate()不应该构造对象!- 违反了 C++ allocator 的基本设计原则
- 容器(如
std::map)期望从allocate()得到未初始化的内存 - 然后通过
construct()用正确的参数构造对象
修改文件清单
- ✅
include/libipc/imp/uninitialized.h- 分离零参数构造(虽然最终不需要) - ✅
include/libipc/mem/container_allocator.h- 修复 allocator 语义(真正的修复)
关键教训
- ❌ 不要在
allocate()中构造对象 - ✅ 严格遵守 allocator 的语义分离:
allocate= 分配内存construct= 构造对象destroy= 销毁对象deallocate= 释放内存
- ✅ 不要假设所有类型都有默认构造函数
- ✅ 标准库容器会调用
construct()传递正确参数