std::vector插入地址异常?递归节点树GCC兼容性问题
问题现象
实现递归函数AddNode,用于将Assimp的aiNode树转换为自定义MeshTreeNode节点树,通过std::vector存储节点并使用索引关联。代码在Windows环境运行正常,但在Debian+GCC环境中,NodeTree虽已填充所有节点,但所有节点的ChildNodes向量均为空。
原核心代码片段
int AddNode( ModelDescriptor &LoadedModelDescriptor, const int ParentIndex, const aiNode *CurrentAssimpNode ) { int NewIndex = (int) LoadedModelDescriptor.NodeTree.size(); LoadedModelDescriptor.NodeTree.push_back( MeshTreeNode() ); LoadedModelDescriptor.NodeTree[NewIndex].Name.assign( CurrentAssimpNode->mName.C_Str() ); LoadedModelDescriptor.NodeTree[NewIndex].ParentNode = ParentIndex; LoadedModelDescriptor.NodeTree[NewIndex].DefaultTransformationMatrix = glm::transpose( glm::make_mat4( (float *) &CurrentAssimpNode->mTransformation ) ); LOG_DEBUG("processing node %u %s %u meshes %u children", NewIndex, LoadedModelDescriptor.NodeTree[NewIndex].Name.c_str(), CurrentAssimpNode->mNumMeshes, CurrentAssimpNode->mNumChildren ); for ( unsigned MeshIterator = 0; MeshIterator < CurrentAssimpNode->mNumMeshes; ++MeshIterator ) { LoadedModelDescriptor.NodeTree[NewIndex].MeshIndices.push_back( CurrentAssimpNode->mMeshes[MeshIterator] ); } for ( unsigned ChildIterator = 0; ChildIterator < CurrentAssimpNode->mNumChildren; ++ChildIterator ) { LoadedModelDescriptor.NodeTree[NewIndex].ChildNodes.push_back( AddNode( LoadedModelDescriptor, NewIndex, CurrentAssimpNode->mChildren[ChildIterator] ) ); } LOG_DEBUG("finished processing node %u %s %u meshes %u children", NewIndex, LoadedModelDescriptor.NodeTree[NewIndex].Name.c_str(), LoadedModelDescriptor.NodeTree[NewIndex].MeshIndices.size(), LoadedModelDescriptor.NodeTree[NewIndex].ChildNodes.size()); return NewIndex; }
修复后的代码片段
将子节点添加逻辑拆分为先接收返回值再执行push_back后,问题解决:
int ChildNodeIndex = AddNode( LoadedModelDescriptor, NewIndex, CurrentAssimpNode->mChildren[ChildIterator] ); LoadedModelDescriptor.NodeTree[NewIndex].ChildNodes.push_back( ChildNodeIndex );
原因解析
问题根源在于C++标准未明确规定函数参数的求值顺序,不同编译器的执行逻辑存在差异:
- Windows平台的MSVC通常会先计算左值(即先获取
LoadedModelDescriptor.NodeTree[NewIndex].ChildNodes的引用),再执行AddNode递归调用。 - Debian下的GCC(部分版本)会先执行
AddNode函数,再获取ChildNodes的引用。
当AddNode递归调用时,会向LoadedModelDescriptor.NodeTree中push_back新节点,若NodeTree当前容量不足,std::vector会触发内存重分配:重新开辟更大的内存块,拷贝原节点数据后释放旧内存。此时旧内存中的MeshTreeNode对象引用会完全失效。
在GCC的执行顺序下,先调用AddNode可能导致NodeTree重分配,后续访问的LoadedModelDescriptor.NodeTree[NewIndex].ChildNodes是指向旧内存的失效引用,push_back操作实际是对已释放内存写入,最终表现为ChildNodes向量为空(属于未定义行为的一种表现)。
修复后的代码先执行AddNode并保存返回值,此时即使NodeTree发生重分配,后续访问LoadedModelDescriptor.NodeTree[NewIndex].ChildNodes时,是通过索引重新获取当前有效的MeshTreeNode对象,再执行push_back,彻底避免了失效引用的问题。
相关结构定义
struct MeshTreeNode { std::string Name; glm::mat4 DefaultTransformationMatrix; std::vector <uint32_t> MeshIndices; std::vector <int> ChildNodes; int ParentNode; }; struct ModelDescriptor { std::string Name; std::vector <MeshTreeNode> NodeTree; std::vector <MeshDescriptor> Meshes; std::vector <LightDescriptor> Lights; BoundingBox BBox; };
内容的提问来源于stack exchange,提问作者Joao Pincho

