c++ - 如何解决指针别名问题?

标签 c++ pointers pointer-arithmetic

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

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

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

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

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

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

我没有处理这个特定的库,而是从这里遇到相同问题的旧二叉树库中提取了一个更简单的提取物。在 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 视为新的根。我想这就是当您过分依赖几个月前完成的调查的内存时会发生的情况。

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

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

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

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

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

最佳答案

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

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

In particular, an object of one type is assumed never to reside at the same address as an object of a different type, unless the types are almost the same. For example, an unsigned int can alias an int, but not a void* or a double. A character type may alias any other type.

Pay special attention to code like this:

     union a_union {
        int i;
        double d;
      };

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

The practice of reading from a different union member than the one most recently written to (called “type-punning”) is common. Even with -fstrict-aliasing, type-punning is allowed, provided the memory is accessed through the union type. So, the code above will work as expected. See Structures unions enumerations and bit-fields implementation. However, this code might not:

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

Similarly, access by taking the address, casting the resulting pointer and dereferencing the result has undefined behavior, even if the cast uses a union type, e.g.:

     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”禁用此特定优化。不过最好修复你的代码;-)

关于c++ - 如何解决指针别名问题?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/3674814/

相关文章:

c++ - 递归最大S个整数

c - 在C中实现2个具有相同类型和名称但参数不同的函数

c++ - 如何在 boost::multi_index::multi_index_container 中存储元素?

c++ - 在 Windows 上的 Qt Creator 中编译 Cuda 代码

c++ - 如何防止创建临时对象?

c - 指针和数组的微妙概念

c - 用于理解指针算术的 OpenCV 视觉效果

c++ - 如何在 QTableView 中使用就地 QComboBox

c - 读取一串整数并用它们创建一个数组

C++ 指针加法被乘