【问题标题】:std::map nodes alignmentstd::map 节点对齐
【发布时间】:2026-02-08 05:50:01
【问题描述】:

我正面临着一件非常了不起的事情。 (场景:Win7 pro 64位,VC2008编译32位代码)

假设一个主程序实例化一个使用 std::map 的类。
这是一个 std::map<:string> 类 Props 只有两个成员:

class Props {
public:
  /**
   * value of property
   * @var boost::any 
   */
  boost::any _a ;

  /**
   * expire time
   * @var time_t
   */
  time_t _tExpiry ;

  /* Somethign more I'm not writing down here... */
}

现在...我构建了一个 DLL,为自己的业务使用相同的类。
DLL 实例化该类并提供 std::map。

嗯...当主程序提供地图时一切正常,而 DLL 在第一个项目插入后崩溃。

还有一些(非常有趣的)。
如果我深入到主程序完成的插入操作,我会将单个 _Node 的构造函数放入 std::map
_Node出现如下(c:\Programmi\Microsoft Visual Studio 9.0\VC\include\xtree)

struct _Node
    {   // tree node
    _Node(_Nodeptr _Larg, _Nodeptr _Parg, _Nodeptr _Rarg,
        const value_type& _Val, char _Carg)
        : _Left(_Larg), _Parent(_Parg), _Right(_Rarg),
            _Myval(_Val), _Color(_Carg), _Isnil(false)
        {   // construct a node with value
        }

    _Nodeptr _Left; // left subtree, or smallest element if head
    _Nodeptr _Parent;   // parent, or root of tree if head
    _Nodeptr _Right;    // right subtree, or largest element if head
    value_type _Myval;  // the stored value, unused if head
    char _Color;    // _Red or _Black, _Black if head
    char _Isnil;    // true only if head (also nil) node
    };

很好...所以我们的 _Node 结构有 _Left(4 字节)、_Parent(4 字节)、_Right(4 字节)、_Myval(地图的键和值的大小)、_Color(1 字节)和 _Isnil(1 字节) .
现在...我要为地图提供的元素是一对 <:string props>。
根据我的调试器,std::string 需要 0x20 个字节,而 Props 只需要 8 个。

现在,当我询问调试器单个节点的大小时,我可以看到它是 0x38。 所以...0x4 + 0x4 + 0x4 + 0x20 + 0x8 + 0x1 + 0x1 = 0x36 + 填充 = 0x38。
这意味着 _Color 成员在 _Node 开始后的 0x34 字节处开始。

(好吧...我宁愿指定我的所有项目都使用 /Zp4,因此所有结构都打包为 4 个字节)。

让我们继续...当我遵循 DLL 行为时,我可以看到一些非常惊人的东西。
std::map 的 _Buynode() 方法调用分配器为我要插入的新元素设置新的大小。调用分配器而不是调用 _Node() 就地构造函数....并且它以另一种方式起作用!!

在这种情况下,构造函数表现为 _Color 成员在 _Node 开头的 0x38 字节之后开始...就好像有不同的填充一样。
在此之后,在以下尝试插入新值时,该过程失败,因为 _Color 和 _Isnil 的值...错误(0xcc,因为该部分内存未初始化)。

我确定我将所有项目中的 /Zp4 设置为解决方案,所以...
怎么了?
我“感觉”对齐有问题,但我不能说是什么......

提前致谢!


好的...我要添加更多内容。
这是来自 c:\Programmi\Microsoft Visual Studio 9.0\VC\include\xtree 的 _Node 结构

struct _Node
    {   // tree node
    _Node(_Nodeptr _Larg, _Nodeptr _Parg, _Nodeptr _Rarg,
        const value_type& _Val, char _Carg)
        : _Left(_Larg), _Parent(_Parg), _Right(_Rarg),
            _Myval(_Val), _Color(_Carg), _Isnil(false)
        {   // construct a node with value
        }

    _Nodeptr _Left; // left subtree, or smallest element if head
    _Nodeptr _Parent;   // parent, or root of tree if head
    _Nodeptr _Right;    // right subtree, or largest element if head
    value_type _Myval;  // the stored value, unused if head
    char _Color;    // _Red or _Black, _Black if head
    char _Isnil;    // true only if head (also nil) node
    };

_Tree_nod(const key_compare& _Parg,
    allocator_type _Al)
    : _Traits(_Parg, _Al), _Alnod(_Al)
    {   // construct traits from _Parg and allocator from _Al
    }

typename allocator_type::template rebind<_Node>::other
    _Alnod; // allocator object for nodes
};

如您所见,它的构造函数非常简单。
当我按照之前所说的那样查找 std::map 时,查看从这个结构中使用的内存。
- _Left
4 个字节 - _Parent
4 个字节 - _Right
4 个字节 - _Myval 为 0x28 字节(std::string 为 0x20,我自己的类为 8。我检查过,它总是 8 字节)
- _Color
的 1 个字节 - 1 个字节用于 _Isnil

当一个新元素放置在树的顶部(我的意思是最左边)被创建时,这个构造函数填充 _Left、_Parent、_Right、_Myval,而不是 lat 4 个未初始化的字节(比如填充 0xcc)并填充它认为的内容应该提前 _Color 和 _Isnil 4 个字节。
最荒谬的是 std::map 的其他方法没有“感觉”这 4 个字节。
这些是导致 de 断言的行。

        if (_Isnil(_Ptr))
        {
            _Ptr = _Right(_Ptr);    // end() ==> rightmost
            if (_Isnil(_Ptr))
#if _HAS_ITERATOR_DEBUGGING
            {
                _DEBUG_ERROR("map/set iterator not decrementable");
                _SCL_SECURE_OUT_OF_RANGE;
            }
#elif _SECURE_SCL
            {
                _SCL_SECURE_OUT_OF_RANGE;
            }
#else
            return; // begin() shouldn't be incremented, don't move
#endif
        }

发生这种情况是因为对 _IsNil(_Ptr) 的测试发现 _Isnil 为 0xcc,并给出了错误。发布版本可能不疯,这不是一件快乐的事。

有什么想法吗?


又进了一步!
我把整个解决方案(2个大项目,18个小项目,大约250000 C/C++代码行),在Linux下编译(gcc4.1.2)。
一切都很好。完全没有问题,std::map 可以正常工作。
我不得不说 Linux Makefile 同时让一切变得更加简单和复杂。复杂是因为你必须自己做所有事情,简单是因为如果你不想做就什么都不会发生。

这告诉我一件事:Visual Studio 2008 中存在一些问题,在某些特定条件下会跳到舞台上……问题是“导致这种情况的条件是什么?”。

等待一个想法...

【问题讨论】:

  • 我们使用的许多结构,其中数据包是相关的,被序列化到磁盘并从中读取。还有另一个原因……过去的兼容性和平台可移植性。即使在 Linux、SunOs、MacOS、Aix 上也可以编译相同的代码,并且可以使用相同的文件。现在...我不得不说我还没有尝试过 Linux...也许这是我要做的下一步。
  • MSVC 是否没有与 gcc 的 __attribute__((packed)) 等效的东西(可能是#pragma pack?),这样您就不必依赖编译器标志了吗? [也:s/memory-alignment/memory-layout/]
  • @user611775 MSVC 使用#pragma pack(push, 4) .... #pragma pack(pop)
  • @Sjored。查看 MSVC 包含文件,我看到了 #pragma pack(push,...) 和 #pragma pack(pop) 的用法。这让我认为 std 库中发生的一切都应该以相同的方式表现,尤其是在对齐方面......但最荒谬的是 _Node cotuctor 使用对齐,而 std::map 的其他方法使用另一个!跨度>

标签: c++ stdmap memory-alignment


【解决方案1】:

我只是猜测,但我的直接猜测是您的问题并非源于对齐问题。

使用 VC++,如果你把这样的东西放在一个 DLL 中,你必须编译代码(主程序和 DLL)才能在 DLL 中使用标准库。执行此操作时,主程序和 DLL 共享一个公共堆,因此它们可以分配/释放内存并保持同步。

如果您静态链接到标准库,您的主程序和您的 DLL 将有单独的堆。当/如果其中的代码尝试删除在另一个中分配的项目(例如),您将遇到重大问题,因为它会尝试将其返回到它不是来自的堆中第一名。

【讨论】:

  • 这是正确的,这就是为什么我们停止链接标准库而只使用“MultiThreaded-DLL”作为发布模块和“MultiThreaded Debug-DLL”作为调试模块。我会检查这方面是否有问题,但我很确定所有模块都使用相同的方法来访问标准库。
  • @Mr. Gate:好吧——这只是一个一直弹出的,所以我认为值得一提......
【解决方案2】:

编译器选项是/Zp4。您确定在所有情况下都使用了正确的选项吗?

【讨论】:

  • 对不起,我的错,我写了 /Z4 但我应该写 /Zp4。每个项目都使用 /Zp4。
【解决方案3】:

您的描述肯定指向对齐/包装问题的方向。

也许您可以在sizeof(Props) == 8 上添加编译时检查。 Boost 有一个编译时检查模板,但在互联网上找到一个或自己动手并不难。

如果 sizeof 始终相同,请让我指向 checked iterators 以检查替代方法。

在那之后,我的想法已经不多了。

【讨论】:

  • 好的,我试试,我也试试升级到Visual Studio 2010,在linux下编译运行。
  • 好的,我会尝试,我也会尝试升级到 Visual Studio 2010 并使用 gcc 在 Linux 下编译和运行。如果你们中的任何人遇到类似的事情,我只是在徘徊,如果有人能告诉我这是一个 VS2008-SP1 问题。