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

11 KiB
Raw Blame History

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 使用红黑树实现,节点类型的设计原则:

  1. 节点总是通过 allocator 的 construct() 构造,而不是 allocate()
  2. 节点必须包含具体的 key-value 数据,不能"空"构造
  3. 删除默认构造函数防止误用
// 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 (在某些情况下)

为什么会有这个差异?

  1. MSVC 的实现细节

    • MSVC 的 _Tree_node 可能有受保护的条件编译的默认构造函数
    • std::is_constructible 检查的是"公开可访问的构造函数"
    • 但实际上,通过 placement new编译器仍然可以调用隐式生成的构造函数
  2. C++ 标准的灰色地带

    • C++14 标准对于 std::is_constructible 的具体实现有一定的解释空间
    • MSVC 的实现更保守,只有明确可访问的构造函数才返回 true
    • GCC 的实现更宽松,只要类型理论上可构造就返回 true

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 上

    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++ 的设计特性:

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)...);
}

优势

  1. 零参数情况:直接使用 T(),避开了 std::is_constructible 的判断差异
  2. 有参数情况:通过 SFINAE 正确分发到直接初始化或聚合初始化
  3. 跨编译器兼容:不依赖编译器对 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);
}

为什么这个修复能解决问题?

  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() 传递正确参数