通过在迭代指针键控映射上出错来捕获不确定性

Posted

技术标签:

【中文标题】通过在迭代指针键控映射上出错来捕获不确定性【英文标题】:Catch nondeterminism by error'ing on iterating pointer-keyed maps 【发布时间】:2019-07-22 17:28:10 【问题描述】:

有几次我们在我正在处理的代码库中发现了不确定性问题,到目前为止,这几乎是使用 std::[unordered_]map/set<T*,U> 的根本原因,其中键是指针,结合迭代映射,通常采用基于范围的 for 循环形式(由于指针值可能在执行之间发生变化,因此迭代顺序是不确定的)。

我想知道当在这样的容器上调用begin() 时,是否有一些黑色模板魔法可以用来注入static_assert。我认为begin() 是执行此操作的最佳位置,或者可能是iterator::operator++,因为以其他方式构造迭代器(例如find() 的结果)是可以的。

我认为我可以重载std::begin,但基于范围的 for 循环的规则表明如果存在.begin(),则使用它。所以,我没有想法。有什么巧妙的技巧可以做到这一点?

进一步说明:不涉及自定义比较器,指针的直接值(也就是目标对象的地址)是关键。这对于插入和查找来说很好,并且只有在迭代容器时才会成为问题,因为顺序基于不可预测的指针值。我试图在现有的大型代码库中找到这样的现有案例。

【问题讨论】:

不要在容器中存储指针?使用取消引用指针的自定义比较/哈希,因此排序基于指向的内容而不是指针值? 您可以编写一个 clang_tidy 检查器 (clang.llvm.org/extra/clang-tidy) 寻找基于范围的 for 循环,该循环使用一组特定的模板参数迭代 unordered_map(或 set)。那不是static_assert,所以它不会找到新添加的案例,但它可以提供帮助。 记住 map/set 的键必须是 const。如果您使用指针作为键,则不得更改顺序(例如更新用于比较键的指向数据) @NathanOliver 你是对的,这就是为什么我说如果 OP 使用自定义比较器,他应该知道这一点。 @Trillian 我只能建议封装:将您的地图存储在隐藏它们的类中,并且只允许用户访问“合法”操作。但我意识到这个选项可能不是你可以选择的,因为它需要大量的代码返工。 【参考方案1】:

您几乎可以通过部分特化实现所需的行为:

20.5.4.2.1 如果 C++ 程序将声明或定义添加到命名空间 std 或命名空间 std 内的命名空间,则 C++ 程序的行为是未定义的,除非另有说明。只有当声明依赖于用户定义的类型并且特化满足原始模板的标准库要求并且没有明确禁止时,程序才能将任何标准库模板的模板特化添加到命名空间 std。

因此,可以使用 std::map 的一个简单特化来检测使用指针键类型实例化模板的尝试:

#include <map>

namespace internal

  // User-defined type trait
  template<class Key, class T>
  class DefaultAllocator
  
  public:
    using type = std::allocator<std::pair<const Key, T>>;
  ;

  // Effectively the same as std::allocator, but a different type
  template<class T>
  class Allocator2 : public std::allocator<T> ;


namespace std

  // Specialization for std::map with a pointer key type and the default allocator.
  // The class inherits most of the implementation from
  // std::map<Key*, T, Compare, ::internal::Allocator2<std::pair<Key*, T>>>
  // to mimic the standard implementation.
  template<class Key, class T, class Compare>
  class map<Key*, T, Compare, typename ::internal::DefaultAllocator<Key*, T>::type> :
    public map<Key*, T, Compare, ::internal::Allocator2<std::pair<Key*, T>>>
  
    using base = map<Key*, T, Compare, ::internal::Allocator2<std::pair<Key*, T>>>;
    using base::iterator;
    using base::const_iterator;

  public:
    // Overload begin() and cbegin()
    iterator begin() noexcept
    
      static_assert(false, "OH NOES, A POINTER");
    
    const_iterator begin() const noexcept
    
      static_assert(false, "OH NOES, A POINTER");
    
    const_iterator cbegin() const noexcept
    
      static_assert(false, "OH NOES, A POINTER");
    
  ;


int main()

  std::map<int, int> m1;
  std::map<int*, int> m2;

  // OK, not a specialization
  m1[0] = 42;
  for (auto& keyval : m1)
  
    (void)keyval;
  

  m2[nullptr] = 42;       // Insertion is OK
  for (auto& keyval : m2) // static_assert failure
  
    (void)keyval;
  

然而,

我还没有想出一种方法来为自定义分配器扩展它:特化的声明必须依赖于一些用户定义的类型。 这是一个可怕的组合,所以我只会使用它来查找现有案例(而不是作为静态检查器保留)。

【讨论】:

这就是我希望看到的东西哈哈!使用不同的模板参数来重用基类的好技巧。我不会永久保留它,但它可能会作为我感兴趣的案例的一次性检查。谢谢!

以上是关于通过在迭代指针键控映射上出错来捕获不确定性的主要内容,如果未能解决你的问题,请参考以下文章

C ++ - 通过getter函数在单独的类中从对象指针映射访问成员函数

5指针

双指针法

双指针法

在 C++ 中通过指针捕获异常

Windows 中的多线程 - 创建函数指针数组时出错