【问题标题】:Find a void pointer in an container of MyClass pointers?在 MyClass 指针的容器中找到一个 void 指针?
【发布时间】:2019-01-05 18:05:08
【问题描述】:

我有一个指针 void* p 指向一个未知类型的变量,还有一个容器 std::set<MyClass*> c 填充有 MyClass 指针。有没有办法找出c是否包含p(即它是否包含指向与p相同的内存地址的指针)而不需要手动循环遍历c中的元素,这不会导致未定义的行为? (请注意,如果我在 c 中找不到它,我不会取消引用 p。)

另外,如果p 指向的数据类型的变量与MyClass 无关,我假设将p 转换为MyClass* 会导致未定义的行为,但也许情况并非如此?

【问题讨论】:

  • 请注意,std::set 的指针使用 指针本身 作为“键”,而不是它所指向的。所以你需要做的就是调用它的find函数来查看集合是否包含指针。
  • 你能改成std::set<MyClass*, std::less<void>>吗?然后您可以使用将 void 指针作为比较器的 find 方法将是 "transparent".
  • @Someprogrammerdude:但指针类型不同......并且演员可能是 UB IIRC。
  • @Jarod42 我认为您的建议可能是赢家! (你应该把它写成一个实际的答案。)我星期一回去工作时必须尝试一下。
  • @HelloGoodbye:我已经发布了答案 ;-)

标签: c++ casting undefined-behavior void-pointers


【解决方案1】:

如果 [..]

它可能导致 UB 是迂腐正确的。

但应该可以在实践中发挥作用(UB 的喜悦)。

有什么方法可以在不手动循环遍历 c 中的元素的情况下找出 c 是否包含 p [..]。

std::find_ifstd::binary_search 可以与适当的谓词一起使用以在线性时间内找到它(std::set::iterator 不是随机迭代器,因此“伪造”binary_search 复杂性)。

如果您可以将容器更改为:

  • std::set<MyClass*, std::less<void>>,那么你可以放心地使用std::set::find 感谢透明的比较器。

  • std::vector<MyClass*> 进行排序,然后您可以使用具有正确复杂度的std::binary_search

【讨论】:

  • “应该可以工作”对于具有未定义行为的代码并不能真正让人放心。它可能在任何时候编译成产生错误的东西,而且可能很难找到,因为你的程序现在做的事情与代码中描述的不同。
  • 我同意 “应该可以工作” 是危险的,但不幸的是,有几个 UB 工作或似乎在实践中工作。
  • 完整的上下文在问题中:-)。我改变了报价的一部分。希望它现在对你有足够的背景。
  • 通过使用std::set<MyClass*, std::less<void>> 甚至std::set<MyClass*, std::less<>> 而不是std::set<MyClass*> 指定比较函子有效!非常感谢! :)
【解决方案2】:

您可以安全地将void* 转换为另一个指针,但不应取消引用它。在 std::set<T*> 中查找指针不会取消引用指针(除非您指定了自定义比较谓词)。

迂腐地,C++ 标准说仅仅加载一个无效的指针是未定义的行为。但是,这是针对具有segmented addressing(x86 实模式)的硬件架构的规定,其中加载指针会将其值的一部分加载到段寄存器中,这可能会导致硬件陷阱。在具有平面内存模型的现代架构中,此限制不适用,加载任何指针值都是明确定义的,因为这只是加载到通用 CPU 寄存器中。

类似:

MyClass* find(std::set<MyClass*> const& c, void* p) {
    auto found = c.find(static_cast<MyClass*>(p));
    return found != c.end() ? *found : nullptr;
}

你可以像这样使用它:

std::set<MyClass*> c;
void* p = ...;
if(MyClass* q = find(c, p))
   // p is found and is q

【讨论】:

  • dynamic_cast 会是一种更清洁的方法(尽管速度较慢)吗?
  • @TvanB dynamic_cast 要求参数的静态类型具有虚函数,而void 没有。
  • @MaximEgorushkin 如果 C++ 标准说只加载无效指针是未定义的行为,恐怕即使在没有优化的情况下编译代码时硬件仍然按预期运行,编译器可能仍然当您使用优化(您通常想要这样做)时生成unexpected assembly code。这应该足以确保您的代码没有任何未定义的行为。
  • @HelloGoodbye 总的来说你是对的。但是,我看不出这在这里如何应用,static_cast 在汇编中是无操作的。你可以想出一个例子。
  • 我对编译器了解得不够多,不知道这是否会出错。此外,我不明白为什么需要提出一个例子;是我在编写代码,所以我认为是你需要说服我 static_cast 在这种情况下是故障安全的,特别是因为你提倡使用可能会产生未定义行为的代码。
【解决方案3】:

在实践中,您应该能够做到(参见其他 anser 和 cmets),但您需要注意一些陷阱。

如果你的类派生自另一个类(比如OtherClass)和MyClass,那么你需要确保p 指向MyClass 部分而不是OtherClass。虽然您不会有未定义的行为,但您不会找到该项目。

class OtherClass { int someData; };
class WontWork : public OtherClass, public MyClass { };

auto *w = new WontWork();
c.insert(w);
void *p = w;

在这种情况下,您不会在集合中找到p,因为p 的值与插入集合中的值不同(在隐式转换为MyClass * 之后)。

要使代码正常工作,您需要 p 指向 MyClass 部分,方法是:

void *p = static_cast<MyClass *>(w);

MyClass *mc = w;
p = mc;

但是,您必须确保不要使用 p,就好像它在代码的其他地方是 OtherClassWontWork 类型一样。

如果可能,最好通过修复声明来完全避免void *

【讨论】:

  • 好吧,现在我没有指向派生自MyClass 的类的对象的指针,而是void 指针。另外,请参阅我对另一个答案的评论,解释为什么投射 void 指针可能不是一个理想的解决方案。
  • 虽然优化可能会因为你对编译器撒谎而导致未定义的行为,但在实践中,由于强制转换可能会隐藏程序员错误,例如导致 this 指针的隐式强制转换,因此出现问题的可能性要大得多在我的示例中进行调整。
  • 有趣的是,在指针类型之间进行转换时,您可能会得到指针调整——我以前从未见过。是否有一些页面描述了它是如何工作的?不过,在这种情况下,我仍然不明白这有什么关系。我没有在子类和超类之间进行转换,所以我不会进行指针调整,对吧?
  • 嗯,还有一个使用void *的原因,因为很多人都没有意识到这一点。请参阅Does this pointer adjustment occur for non-polymorphic inheritance?Base pointer offset adjustment for multiple inheritance 了解说明。
  • 本质上,如果您将类型转换为void*,那么您必须转换为完全相同的类型(而不是基类、兄弟类或派生类)才能将行为定义为布局是实现定义。 标准 会更精确地定义它,但如果可能的话,最好避免强制转换和转换为void *。如果没有,则尽可能进行本地化(理想情况下,每个方向都在一个地方进行转换),因为如果需要更新层次结构,它可以简化维护。虚拟表的存在等因素也会影响布局。
猜你喜欢
  • 2019-09-27
  • 2014-05-30
  • 1970-01-01
  • 2016-02-15
  • 2016-11-23
  • 1970-01-01
  • 2013-12-18
  • 2021-07-22
  • 2016-03-21
相关资源
最近更新 更多