C++ 通过错误捕获不确定性';迭代指针键控映射的迭代

C++ 通过错误捕获不确定性';迭代指针键控映射的迭代,c++,stl,iterator,C++,Stl,Iterator,有几次我们在我正在研究的代码库中发现了不确定的问题,到目前为止,这几乎是由于使用了std::[unordered_uu]map/set,其中键是指针,与映射上的迭代相结合,通常是以基于范围的for循环的形式(由于指针值可能在执行之间发生变化,因此迭代顺序是不确定的) 我想知道在这样一个容器上调用begin()时,是否有一些黑模板魔法可以用来注入static\u assert。我认为begin()是这样做的最好地方,或者可能是iterator::operator++,因为构建迭代器不是这样的,比如

有几次我们在我正在研究的代码库中发现了不确定的问题,到目前为止,这几乎是由于使用了
std::[unordered_uu]map/set
,其中键是指针,与映射上的迭代相结合,通常是以基于范围的for循环的形式(由于指针值可能在执行之间发生变化,因此迭代顺序是不确定的)

我想知道在这样一个容器上调用
begin()
时,是否有一些黑模板魔法可以用来注入
static\u assert
。我认为
begin()
是这样做的最好地方,或者可能是
iterator::operator++
,因为构建迭代器不是这样的,比如
find()
,没问题

我原以为我可以重载
std::begin
,但基于范围的for循环规则规定,如果存在
.begin()
,则使用它。因此,我没有主意了。有没有聪明的方法可以做到这一点


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

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

<> P.20.5.4.2.1如果在指定的名称空间STD或名称空间STD中添加声明或定义,除非另有指定,否则C++程序的行为是未定义的。除非声明依赖于用户定义类型和专用程序,否则程序可以将任何标准库模板的模板特化添加到命名空间STD。TIONE符合原始模板的标准库要求,且未明确禁止

因此,可以使用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;
  }
}
#包括
命名空间内部
{
//用户定义类型特征
模板
类DefaultAllocator
{
公众:
使用type=std::分配器;
};
//实际上与std::allocator相同,但类型不同
模板
类分配器2:公共std::分配器{};
}
名称空间标准
{
//使用指针键类型和默认分配器专门化std::map。
//该类从继承了大部分实现
//标准::地图
//模仿标准实现。
模板
类图:
公共地图
{
使用base=map;
使用base::iterator;
使用base::const_迭代器;
公众:
//重载begin()和cbegin()
迭代器begin()noexcept
{
static_断言(false,“哦,不,指针”);
}
常量迭代器begin()常量noexcept
{
static_断言(false,“哦,不,指针”);
}
常量迭代器cbegin()常量noexcept
{
static_断言(false,“哦,不,指针”);
}
};
}
int main()
{
std::map m1;
std::map m2;
//好的,不是专门的
m1[0]=42;
用于(自动和键值:m1)
{
(无效)基瓦尔;
}
m2[nullptr]=42;//插入正常
for(auto&keyval:m2)//静态断言失败
{
(无效)基瓦尔;
}
}
但是,

  • 我还没有找到一种方法来扩展自定义分配器:专门化的声明必须依赖于某些用户定义的类型
  • 这是一个可怕的乱七八糟的问题,所以我只会使用它来查找现有的案例(而不是作为静态检查器保存)

不在容器中存储指针?使用自定义比较/散列来取消引用指针,因此排序基于指向的内容而不是指针值?您可以编写一个
clang_tidy
检查器(),查找在
无序映射
(或
集合
)上迭代的循环的范围使用一组特定的模板参数。这不是一个
静态断言
,因此它找不到新添加的大小写,但可能会有所帮助。请记住,映射/集的键必须是常量。如果使用指针作为键,则不得更改顺序(例如,更新用于比较键的指向数据)@NathanOliver你是对的,这就是为什么我说如果OP使用自定义比较器,他应该知道这一点。@Trillian我只能建议封装:将地图存储在一个隐藏它们的类中,并且只允许用户访问“合法的”操作。但我意识到这个选项可能不是你可以选择的,因为它需要大量的代码返工。这正是我希望看到的。哈哈!使用不同的模板参数重用基类的好方法。我不会永久保留这个选项,但它可能会作为我感兴趣的情况的一次性检查艾德进来了,谢谢!