代码之家  ›  专栏  ›  技术社区  ›  Steve314

如何解决指针别名问题?

  •  2
  • Steve314  · 技术社区  · 15 年前

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

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

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

    这是一个问题,尽管-随着乐观主义者变得更积极,这可能导致指针别名问题。如果节点可以从库中退出,则情况尤其如此。例如,可以从二叉树中提取节点并重新链接以形成链接列表。另一个令人恼火的例子是在单元测试时发生的。

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

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

    我没有处理这个特定的库,而是从这里的一个旧的二叉树库中提取了一个更简单的内容,它也遇到了同样的问题。在VC++9中,它只是起作用。使用mingwGCC4.4.0,调试构建可以工作,但发布构建失败。问题是,优化器发现指针别名的内联和失败的混合。

    只是说清楚-我不想在这里“wtf-goto!!或者什么。该问题正在解决优化/指针问题。但是如果你能找到写作的方法 Tree_To_List 这是正确的结构,不使用隐藏/伪装的goto来实现,我感兴趣。

    此外,还缺少一个基于模板的抽象层(模板c_-bin_-tree_工具不能完成整个工作-c_工具完成包装,但以每次使用的方式完成,而不是以可重用的形式完成。这只是提取相关代码的副作用。

    这段代码所做的是通过逐个插入节点来创建一个不平衡的二叉树,然后平衡该树。平衡的工作方式是将树转换为列表(以某种方式已经是列表),然后将列表转换回树。树在平衡前后都被倾倒到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
    

    实际发生的事情(mingwGCC4.4.0,优化的发布构建)

     1
     . 2
     . . 3
     . . . 4
     . . . . 5
     . . . . . 6
     . . . . . . 7
     . . . . . . . 8
     . . . . . . . . 9
     . . . . . . . . . 10
    ----------
     1
    

    据我所知,平衡操作运行正常,但bt_dump函数看不到 m_Left m_Right 领域。

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

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

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

    一个解决办法是让不安全的代码通过getter和setter函数指针执行所有读写操作,避免所有指针算法,并确保所有对C节点实例的访问都是通过C节点*指针完成的。更妙的是,接口变成了一个带有getter/setter方法等的类。在完整的二进制树库中,我有其他的策略类来实现这一点,不过老实说,当问题出现时,我真正的解决办法是将所有代码都扔到一个“垃圾”文件夹中,而我很少使用它,而且可能在任何情况下都应该使用boost侵入列表。哎呀。

    然而,这仍然会留下另一个更复杂、使用量更大的容器库,而这并不会消失。我想我只需要做非常痛苦的重构就可以去掉offsetof和pointer的算术运算,但是…

    C++规则究竟是什么——当编译器精确地不可能发现一个可能的指针别名时?是否可以重写上面的二叉树代码,以便它仍然使用指针算法,仍然允许在库内和库外访问/修改节点,但不存在这种优化问题?

    2 回复  |  直到 15 年前
        1
  •  3
  •   Gunther Piez    15 年前

    你关掉警告了吗?您应该已经得到一些“取消引用类型punned指针违反了严格的别名”,因为这正是您在(void**)ptr_add(…

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

    特别是,假定一个类型的对象永远不会与另一个类型的对象驻留在同一地址,除非类型几乎相同。例如,无符号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别名,只要通过union类型访问内存,也允许使用类型punning。因此,上面的代码将按预期工作。请参见结构联合枚举和位字段实现。但是,此代码可能不会:

         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别名选项在-o2、-o3、-os级别下启用。

    在你的情况下,你可以使用

    union {
        void** ret_ptr;
        ptrdiff_t in_ptr;
    }
    

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

    或者使用“fno严格混叠”禁用此特定优化。但最好修复代码;-)

        2
  •  0
  •   Puppy    15 年前

    不小心使用模板会导致膨胀。但你完全没抓住要点。

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

    你只是一点都没有好好考虑过。模板提供的优势在可执行大小上远远超过了几个KB。

    值得注意的是,该代码在Visual Studio 2010上的工作方式与预期一致。