不小心使用模板会导致臃肿。避免这种膨胀的一种方法是使用一个精简的类型安全模板来包装非类型安全的非模板代码。为此,包装器需要为非模板代码提供某种方式来访问它一无所知的东西。
例如,在数据结构中,包装器定义节点结构。不安全代码需要读取和写入节点,但必须通过包装器指定的某种接口间接地这样做。
实现此接口的一种方法是在结构(由不安全代码定义)中填充详细信息,例如由包装器确定的函数指针和常量。一种相关的常量是特定字段的偏移量(在某些结构内)。不安全的代码可以使用该偏移量(和一些指针算法)直接访问该字段。
但是,这会带来问题 - 随着优化器变得更加积极,这可能会导致指针别名问题。如果节点可以逃离库,情况尤其如此。例如,可以从二叉树中提取节点并重新链接以形成链接列表。另一个令人讨厌的例子是在单元测试时发生。
我目前有一个按照这些思路编写的容器库,目前它不会导致这些问题 - 但很快就会出现。它避免这些问题的原因是因为所有单元测试都应用于容器(而不是底层代码),并且因为节点永远不会逃脱容器。也就是说,节点总是以相同的指针算术方式访问,因此指针别名优化问题永远不会出现。
不幸的是,我很快将需要允许从容器中提取节点,并且我可能还需要对底层不安全代码进行单元测试。
我没有处理这个特定的库,而是从这里遇到相同问题的旧二叉树库中提取了一个更简单的提取物。在 VC++9 中它可以正常工作。使用 MinGW GCC 4.4.0,调试构建工作,但发布构建失败。问题是内联和优化器未能发现指针别名的混合。
只是要清楚 - 我不想在这里“WTF - GOTO !!!” 管他呢。问题是解决优化/指针问题。虽然如果你能找到一种Tree_To_List
结构合理且不使用隐藏/伪装的 goto 来实现它的编写方法,我很感兴趣。
此外,缺少一层基于模板的抽象(模板 c_Bin_Tree_Tool 不能完成整个工作 - c_Tool 完成包装,但以每次使用的方式而不是以可重复使用的形式。这只是一个副作用提取相关代码。
这段代码所做的是通过一个接一个地插入节点来创建一个不平衡的二叉树,然后平衡该树。平衡的工作原理是将树转换为列表(在某种程度上它已经是),然后将列表转换回树。树在平衡前后都被转储到 stdio。
bintree.h
...
inline void* Ptr_Add (void* p1, std::ptrdiff_t p2) { return (void*) (((std::ptrdiff_t) p1) + p2); }
struct c_Bin_Tree_Closure
{
typedef int (*c_Node_Comp) (void* p_Node1, void* p_Node2);
c_Node_Comp m_Node_Comp;
std::ptrdiff_t m_Left, m_Right;
};
class c_BT_Policy_Closure
{
private:
const c_Bin_Tree_Closure* m_Closure;
protected:
void** Left_Of (void* p) { return ((void**) Ptr_Add (p, m_Closure->m_Left )); }
void** Right_Of (void* p) { return ((void**) Ptr_Add (p, m_Closure->m_Right)); }
int Compare_Node (void* p_Node1, void* p_Node2) const
{
return m_Closure->m_Node_Comp (p_Node1, p_Node2);
}
public:
c_BT_Policy_Closure ()
{
m_Closure = 0;
}
void Set_Closure (const c_Bin_Tree_Closure& p_Closure)
{
m_Closure = &p_Closure;
}
};
template<class tc_Policy>
class c_Bin_Tree_Tool : public tc_Policy
{
public:
c_Bin_Tree_Tool ()
{
}
void *List_To_Tree (size_t p_Size, void* &p_List);
void Tree_To_List (void* p_Root, void* &p_First, void* &p_Last, size_t &p_Size);
void Balance (void* &p);
void Insert (void* &p_Tree, void* p_Node);
};
template<class tc_Policy>
void* c_Bin_Tree_Tool<tc_Policy>::List_To_Tree (size_t p_Size, void* &p_List)
{
if (p_Size == 0) return 0;
size_t l_Size = p_Size / 2;
void* l_Ptr = List_To_Tree (l_Size, p_List);
void* l_This = p_List;
p_List = *tc_Policy::Right_Of (l_This);
*tc_Policy::Left_Of (l_This) = l_Ptr;
l_Size = p_Size - (l_Size + 1);
*tc_Policy::Right_Of (l_This) = List_To_Tree (l_Size, p_List);
return l_This;
}
template<class tc_Policy>
void c_Bin_Tree_Tool<tc_Policy>::Tree_To_List (void* p_Root, void* &p_First, void* &p_Last, size_t &p_Size)
{
// Use left links as prev links and right links as next links
void* l_Start = 0; // first-item-in-list pointer
void* l_Prev = 0; // previous node in list
void** l_Prev_Ptr = &l_Start; // points to the next (ie right) pointer for the next node.
void* l_Pos = p_Root;
void* l_Next;
void* l_Parent = 0;
size_t l_Count = 0;
p_Last = 0;
TOP_OF_LOOP:
l_Next = *tc_Policy::Left_Of (l_Pos);
if (l_Next != 0)
{
*tc_Policy::Left_Of (l_Pos) = l_Parent; // So we can find our way back up the tree
l_Parent = l_Pos;
l_Pos = l_Next;
goto TOP_OF_LOOP;
}
AFTER_STEP_PARENT:
l_Next = *tc_Policy::Right_Of (l_Pos);
*tc_Policy::Left_Of (l_Pos) = l_Prev;
p_Last = l_Pos;
l_Prev = l_Pos;
*l_Prev_Ptr = l_Pos;
l_Prev_Ptr = tc_Policy::Right_Of (l_Pos);
l_Count++;
if (l_Next != 0)
{
l_Pos = l_Next;
goto TOP_OF_LOOP;
}
if (l_Parent != 0)
{
l_Pos = l_Parent;
l_Parent = *tc_Policy::Left_Of (l_Pos);
goto AFTER_STEP_PARENT;
}
*l_Prev_Ptr = 0;
p_First = l_Start;
p_Size = l_Count;
}
template<class tc_Policy>
void c_Bin_Tree_Tool<tc_Policy>::Balance (void* &p)
{
void *l_First, *l_Last;
size_t l_Count;
Tree_To_List (p, l_First, l_Last, l_Count);
p = List_To_Tree (l_Count, l_First);
}
template<class tc_Policy>
void c_Bin_Tree_Tool<tc_Policy>::Insert (void* &p_Tree, void* p_Node)
{
void** l_Tree = &p_Tree;
while (*l_Tree != 0)
{
int l_Compare = tc_Policy::Compare_Node (*l_Tree, p_Node);
l_Tree = ((l_Compare < 0) ? tc_Policy::Right_Of (*l_Tree) : tc_Policy::Left_Of (*l_Tree));
}
*l_Tree = p_Node;
*tc_Policy::Right_Of (p_Node) = 0;
*tc_Policy::Left_Of (p_Node) = 0;
};
test_bintree.cpp
...
#include <iostream>
#include "bintree.h"
struct c_Node
{
c_Node *m_Left, *m_Right;
int m_Data;
};
c_Node g_Node0001 = { 0, 0, 1 }; c_Node g_Node0002 = { 0, 0, 2 };
c_Node g_Node0003 = { 0, 0, 3 }; c_Node g_Node0004 = { 0, 0, 4 };
c_Node g_Node0005 = { 0, 0, 5 }; c_Node g_Node0006 = { 0, 0, 6 };
c_Node g_Node0007 = { 0, 0, 7 }; c_Node g_Node0008 = { 0, 0, 8 };
c_Node g_Node0009 = { 0, 0, 9 }; c_Node g_Node0010 = { 0, 0, 10 };
int Node_Compare (void* p1, void* p2)
{
return (((c_Node*) p1)->m_Data - ((c_Node*) p2)->m_Data);
}
c_Bin_Tree_Closure g_Closure =
{
(c_Bin_Tree_Closure::c_Node_Comp) Node_Compare,
offsetof (c_Node, m_Left ), offsetof (c_Node, m_Right)
};
class c_Tool : public c_Bin_Tree_Tool<c_BT_Policy_Closure>
{
protected:
typedef c_Bin_Tree_Tool<c_BT_Policy_Closure> c_Base;
public:
c_Tool () { Set_Closure (g_Closure); }
void Insert (c_Node* &p_Tree, c_Node* p_Node) { c_Base::Insert ((void*&) p_Tree, p_Node); }
void Balance (c_Node* &p) { c_Base::Balance ((void*&) p); }
};
void BT_Dump (size_t p_Depth, c_Node* p_Node)
{
if (p_Node != 0)
{
BT_Dump (p_Depth + 1, p_Node->m_Left);
for (size_t i = 0; i < p_Depth; i++) std::cout << " .";
std::cout << " " << p_Node->m_Data << std::endl;
BT_Dump (p_Depth + 1, p_Node->m_Right);
}
}
int main (int argc, char* argv[])
{
c_Tool l_Tool;
c_Node *l_Root = 0;
l_Tool.Insert (l_Root, &g_Node0001);
l_Tool.Insert (l_Root, &g_Node0002);
l_Tool.Insert (l_Root, &g_Node0003);
l_Tool.Insert (l_Root, &g_Node0004);
l_Tool.Insert (l_Root, &g_Node0005);
l_Tool.Insert (l_Root, &g_Node0006);
l_Tool.Insert (l_Root, &g_Node0007);
l_Tool.Insert (l_Root, &g_Node0008);
l_Tool.Insert (l_Root, &g_Node0009);
l_Tool.Insert (l_Root, &g_Node0010);
BT_Dump (0, l_Root); std::cout << "----------" << std::endl;
l_Tool.Balance (l_Root);
BT_Dump (0, l_Root);
return 0;
}
预期的结果是...
1
. 2
. . 3
. . . 4
. . . . 5
. . . . . 6
. . . . . . 7
. . . . . . . 8
. . . . . . . . 9
. . . . . . . . . 10
----------
. . . 1
. . 2
. 3
. . . 4
. . 5
6
. . . 7
. . 8
. 9
. . 10
实际发生了什么(MinGW GCC 4.4.0,优化的发布版本)......
1
. 2
. . 3
. . . 4
. . . . 5
. . . . . 6
. . . . . . 7
. . . . . . . 8
. . . . . . . . 9
. . . . . . . . . 10
----------
1
据我所知,Balance 操作运行正常,但 BT_Dump 函数无法看到m_Left
和m_Right
字段的所有更改。
编辑那是错误的 - 否则为什么我将节点 1 视为新根。我想这就是当你过分依赖几个月前进行的调查的记忆时会发生的情况。
编辑实际上,节点 1 作为根是一个问题,但由于它是旧根 - 好吧,最好忽略这个问题是什么,并制定你自己的理论;-)
就代码中未定义标准而言,存在许多问题。我认为最大的问题是节点结构中的链接是 c_Node*,但由于不安全的代码对 c_Node 一无所知,因此它(通过指针算法)将它们作为 void* 访问。
一种解决方法是让不安全的代码通过 getter 和 setter 函数指针进行所有读取和写入,避免所有指针算术并确保对 c_Node 实例的所有访问都是通过 c_Node* 指针完成的。更好的是 - 接口变成了一个带有 getter/setter 方法等的类。在完整的二叉树库中,我有替代策略类来执行此操作,但老实说,当问题出现时,我真正的解决方法是将所有代码放入“垃圾”文件夹,因为我很少使用它,并且无论如何应该使用 boost 侵入性列表。
但是,这仍然留下了另一个更复杂且使用率更高的容器库,它并没有消失。我想我将不得不进行非常痛苦的重构来摆脱 offsetof 和指针算术的东西,但是......
C++ 规则到底是什么 - 究竟什么时候允许编译器无法发现可能的指针别名?是否可以重写上面的二叉树代码,使其仍然使用指针算法,仍然允许在库内部和外部访问/修改节点,并且不受这个优化问题的影响?