2

不小心使用模板会导致臃肿。避免这种膨胀的一种方法是使用一个精简的类型安全模板来包装非类型安全的非模板代码。为此,包装器需要为非模板代码提供某种方式来访问它一无所知的东西。

例如,在数据结构中,包装器定义节点结构。不安全代码需要读取和写入节点,但必须通过包装器指定的某种接口间接地这样做。

实现此接口的一种方法是在结构(由不安全代码定义)中填充详细信息,例如由包装器确定的函数指针和常量。一种相关的常量是特定字段的偏移量(在某些结构内)。不安全的代码可以使用该偏移量(和一些指针算法)直接访问该字段。

但是,这会带来问题 - 随着优化器变得更加积极,这可能会导致指针别名问题。如果节点可以逃离库,情况尤其如此。例如,可以从二叉树中提取节点并重新链接以形成链接列表。另一个令人讨厌的例子是在单元测试时发生。

我目前有一个按照这些思路编写的容器库,目前它不会导致这些问题 - 但很快就会出现。它避免这些问题的原因是因为所有单元测试都应用于容器(而不是底层代码),并且因为节点永远不会逃脱容器。也就是说,节点总是以相同的指针算术方式访问,因此指针别名优化问题永远不会出现。

不幸的是,我很快将需要允许从容器中提取节点,并且我可能还需要对底层不安全代码进行单元测试。

我没有处理这个特定的库,而是从这里遇到相同问题的旧二叉树库中提取了一个更简单的提取物。在 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_Leftm_Right字段的所有更改。

编辑那是错误的 - 否则为什么我将节点 1 视为新根。我想这就是当你过分依赖几个月前进行的调查的记忆时会发生的情况。

编辑实际上,节点 1 作为根是一个问题,但由于它是旧根 - 好吧,最好忽略这个问题是什么,并制定你自己的理论;-)

就代码中未定义标准而言,存在许多问题。我认为最大的问题是节点结构中的链接是 c_Node*,但由于不安全的代码对 c_Node 一无所知,因此它(通过指针算法)将它们作为 void* 访问。

一种解决方法是让不安全的代码通过 getter 和 setter 函数指针进行所有读取和写入,避免所有指针算术并确保对 c_Node 实例的所有访问都是通过 c_Node* 指针完成的。更好的是 - 接口变成了一个带有 getter/setter 方法等的类。在完整的二叉树库中,我有替代策略类来执行此操作,但老实说,当问题出现时,我真正的解决方法是将所有代码放入“垃圾”文件夹,因为我很少使用它,并且无论如何应该使用 boost 侵入性列表。

但是,这仍然留下了另一个更复杂且使用率更高的容器库,它并没有消失。我想我将不得不进行非常痛苦的重构来摆脱 offsetof 和指针算术的东西,但是......

C++ 规则到底是什么 - 究竟什么时候允许编译器无法发现可能的指针别名?是否可以重写上面的二叉树代码,使其仍然使用指针算法,仍然允许在库内部和外部访问/修改节点,并且不受这个优化问题的影响?

4

2 回答 2

3

你关掉警告了吗?您应该有一些“取消引用类型双关指针违反严格别名”,因为这正是您在 (void**) Ptr_Add(...

编译器可以自由地假设指向不同类型的指针没有别名(有一些执行),并将生成优化的代码,将指针的目标缓存在寄存器中。为避免这种情况,您必须使用联合在不同的指针类型之间进行转换。引用http://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html#Optimize-Options

特别是,假设一种类型的对象永远不会与不同类型的对象驻留在相同的地址,除非类型几乎相同。例如,unsigned int 可以给 int 起别名,但不能为 void* 或 double。字符类型可以为任何其他类型起别名。

特别注意这样的代码:

     union a_union {
        int i;
        double d;
      };

     int f() {
        union a_union t;
        t.d = 3.0;
        return t.i;
      }

从不同的工会成员那里阅读而不是最近写入的成员(称为“类型双关语”)的做法很常见。即使使用 -fstrict-aliasing,也允许使用类型双关语,前提是通过联合类型访问内存。因此,上面的代码将按预期工作。请参阅结构联合枚举和位域实现。但是,此代码可能不会:

     int f() {
        union a_union t;
        int* ip;
        t.d = 3.0;
        ip = &t.i;
        return *ip;
      }

类似地,通过获取地址、转换结果指针和取消引用结果的访问具有未定义的行为,即使转换使用联合类型,例如:

     int f() {
        double d = 3.0;
        return ((union a_union *) &d)->i;
      }

-fstrict-aliasing 选项在 -O2、-O3、-Os 级别启用。

在你的情况下,你可以使用类似的东西

union {
    void** ret_ptr;
    ptrdiff_t in_ptr;
}

但是 ptr_add 中的代码看起来很糟糕;-)

或者只是使用“fno-strict-aliasing”禁用此特定优化。更好地修复你的代码;-)

于 2010-09-09T08:35:37.370 回答
0

不小心使用模板会导致臃肿。但你完全错过了这里的重点。

  • 如果使用不小心,不小心,模板会导致膨胀。
  • 模板避免的运行时错误数量巨大。
  • 模板化代码的速度远远大于非模板化代码。
  • 除非您在嵌入式系统上运行,否则可执行文件的大小绝对是微不足道的。
  • STL 提供了一个地图容器(它是一个二叉搜索树)供您使用。

你只是根本没有正确地考虑到这一点。模板提供的优势远远超过几 kb 的可执行文件大小。

还值得注意的是,代码在 Visual Studio 2010 上按预期工作。

于 2010-09-09T08:35:08.010 回答