如何检查void*是否指向有效实例的对象类型指针?

5
我是一名有用的助手,可以为您翻译文本。
我正在寻找最常见和最可靠的方法来检查是否可以将 void* 转换为给定的 C++ 对象类型。您可以在下面看到一些有关上下文的信息。
当我为 DLL 定义 C API 时,我经常使用 void* 来隐藏我使用的 C++ 对象(类似下面的内容)。
typedef void* Instance_t;
int createInstance(Instance_t* pInst);
int processing(Instance_t inst, uint8_t* pBuf, size_t bufLength);

当我使用createInstance时,代码会像这样强制转换指针:

int createInstance(Instance_t* pInst)
{ 
   MyClass* ptr = new MyClass();
   *pInst = (void*)(ptr);
   //.... etc
   return 0;
 }

但问题是,在我们定义的所有其他C函数中,如何检查我们收到的void*值是否是一个有效的MyClass*。我认为我们不能这样做,因为在这种情况下,没有任何C ++转换运算符是真正类型安全的(即使dynamic_cast)。目前,我最好的解决方案是使用C转换(或reinterpret_cast),如果一切正常,则调用与MyClass一起定义的IsValid函数。您有更好的方法来进行此检查吗?

你如何知道数据是有效的?你在脑海中应用了哪些一致性测试?将其应用于接收到的数据,如果通过测试,则数据可能是正确的。 - RedX
2
前向声明您的类型,并仅在 *.c 源文件中定义它。这样可以避免使用 void *,因为您需要类型检查。 - amdixon
如果您可以接受RTTI,您可以尝试做您想要的事情 - Roger Rowland
@RogerRowland 这只适用于指向多态类型的指针,而不是 void* - Mike Seymour
@MikeSeymour 是的,那是真的,抱歉,它需要一个共同的基础,而不是'void*',而我讨厌'CObject'类型的方法。 - Roger Rowland
在标准的C++中,没有办法确定指针指向什么。即使它指向有效的内存,除非类型以某种方式是多态的并且您有正确类型的基础指针,否则没有任何信息存储在任何地方来确定那块内存是什么。 - PlasmaHH
8个回答

7

除非你从内存池中分配所有MyClass实例并检查传递给你的地址是否是指向该内存池中的指针,或者维护一个有效实例列表,否则你无法做到这一点。

但是,如果需要传递不透明指针,请让客户端使用它。

struct MyClass;
typedef struct MyClass *Instance_t;

这将干净地编译并给您合理的心理安慰。只要您仅使用指针,编译器就会很高兴。当它对其进行解引用时,编译器需要知道指针实际指向的内容。

我相信在正确的 C 代码中,这应该是 typedef struct MyClass * Instance_t - Matthieu M.

5

我认为你不能做这件事,也不应该这样做。 void * 只是指向内存中某个位置的指针。几乎按照定义,无法知道它指向什么。

但是,如果您想防止用户操纵类的内部机制,为什么要将所有内容类型转换为 void *?为什么不使用受保护和私有方法来实现呢?


嗯...在C语言中如何使用protectedprivate关键字? - Matthieu M.
嗯...我们在谈论C++。你也不能在C中使用class或任何对象... - craigmj
1
@craigmj 他在谈论C API。 - Tom Tanner
澄清一下:OP正在询问如何构建他的C API,因此所提供的解决方案需要适应公共API中的C,但可以在其外部使用C++。 - Matthieu M.
啊,我明白了。所以他在 C API 中返回一个 void*,然后当稍后收到它时,想要确保它是指向自己类的指针。这样他就不必担心保护类方法,因为 C API 可能是访问这些方法的唯一途径。但这并不能更接近确定指针...我猜只能回答问题的第一部分了;-) - craigmj

3

无法检查未类型化指针是否指向特定类型的有效对象。如果使用 void*,则会丢失类型检查。相反,您可以在C头文件中声明类型,并使用指向该(不完整)类型的指针,而不是 void*

struct Instance;
int createInstance(struct Instance** pInst);
int processing(struct Instance* inst, uint8_t* pBuf, size_t bufLength);

在 C++ 中,您可以定义并使用类。

// Probably better to use "struct" rather than "class" in case
// some compilers complain about mixing class keys.
struct Instance {
    // Whatever
};

int createInstance(Instance** pInst) {
    *pInst = new Instance;
    // and so on
}

注意:MSVC对于“struct Instance”和“class Instance”的名称重整方式不同,而C++标准建议不要混用“struct”/“class”来表示同一种类型。 - Matthieu M.
@MatthieuM:我从未听说过这个建议。但是如果某个编译器在这方面出现了问题,那么也许最好在两种情况下都坚持使用“struct”。 - Mike Seymour
我不会说是错误的:§7.1.6.3/3 [dcl.type.elab] 中提到在 elaborated-type-specifier 中出现的 class-key 或 enum 关键字应与 elaborated-type-specifier 中引用的声明的类别相符。[...] 并且必须使用 class 或 struct class-key 来引用使用 class 或 struct class-key 声明的类(第9条)。;而且并不完全清楚 classstruct 是否可以互换使用。 - Matthieu M.
@MatthieuM:但是该段落还指定了“同类”,即“或者使用 class 或者 struct class-key 来引用使用 classstruct class-key 声明的类”。这对我来说似乎很清楚。如果必须完全匹配,它将像 unionenum 一样说明。 - Mike Seymour

1

无法确定 void* 是否指向有效的C++类。除非启用了RTTI,否则没有与类相关的元数据,即使启用了RTTI,在C++中也有很多情况下 void* 不指向类。例如:

int x=10;
void *ptr = &x;

这里ptr指向原始值。整数没有关联的RTTI,所以你如何查询它以确定任何内容>


0

当我需要将一些对象从我的CPP库导出到C代码时,我会这样做:

typedef void * OBJ1;
typedef void * OBJ2;

OBJ1 createObj1();
OBJ2 createObj2();

void doObj1(OBJ1 obj);

所以在 do 函数中,我确切地知道要期望哪个对象


这大致就是问题的描述。但正如问题所述,如果您正在使用(void*的别名),则无法防止诸如“do(createObj2());”之类的错误。(此外,您不能将函数命名为“do”,因为那是一个关键字)。 - Mike Seymour
你说得很对,“do”是保留字,我有点匆忙,已经更正了我的答案。因此,像doObj1(createObj2());这样的错误可能是因为没有办法严格检查typedef类型。但至少这是我知道的避免混淆的最好方法。 - sim
我觉得 typedef void * OBJ1; 这个定义很令人困惑。对于编译器来说,这一点也没有帮助。 - alk
比较一下这两个:doObj1( void * o ); doObj2( void * o ); 和这个 doObj1( obj1_t o); doObj2( obj2_t o)。你也可以在typedef中使用"struct"关键字(typedef struct obj1 * obj1_t),这样doObj1( createObj1())将在编译时失败。 - sim
后者有意义,前者假装安全但实际上并不安全。 - alk

0

简短回答:这可能会很困难。

问题很多,但基本上归结为C和C++中可访问的操作非常低级(注意:可访问,但不一定合法)。void*的作用是任何指针都可以强制转换为它,但如果您应该使用另一种类型,滥用reinterpret_cast仍然可能会导致麻烦。


一个简单的解决方案是使用标记。基本上,在指针中放置一个类型 id,这样你就可以始终知道对象的原始类型。虽然这很繁琐(因为每种类型都需要修改),但部署起来很容易。
typedef enum {
    SP_ATag,
    SP_BTag,
    SP_CTag,
    ...
} SP_Tag_t;

// External Tag
typedef struct {
    SP_Tag_t tag;
    void* p;
} SP_Any_t;

// Internal Tag
struct A {
    SP_Tag_t tag;
    ...;
};

...

typedef union {
    A* a;
    B* b;
    C* c;
} SP_Any_t;

然后,您可以使用SP_Any_t代替void*

优点:

  • 轻量级
  • 外部解决方案不需要修改现有类
  • 内部解决方案应与现有C代码二进制兼容

缺点:

  • 声明所有类型的单个位置
  • 损坏标签很容易(无论是意外还是故意的)

一个更复杂的解决方案,可以作为良好的调试帮助,是引入一个每种类型的注册表。缺点是需要对现有类型进行仪器化才能使其工作,但它足够简单,并且涉及更多的运行时开销。但是嘿:它有效
template <typename> class Registrable;

//
// class Registry
//
class Registry {
public:
template <typename> friend class Registrable;

template <typename T>
static T* Cast(void*);

private:
struct TagType {};

using Key = std::pair<TagType const*, void*>;
using Store = std::set<Key>;

template <typename T>
static void Register(Registrable<T>* t);

template <typename T>
static void Unregister(Registrable<T>* t);

static Store& Get();
}; // class Registry

template <typename T>
T* Registry::Cast(void* const pointer) {
TagType const* const tag = &Registrable<T>::Tag;

if (Get().count(std::make_pair(tag, pointer)) == 0) { return nullptr; }

return static_cast<T*>(reinterpret_cast<Registrable<T>*>(pointer));
}

template <typename T>
void Registry::Register(Registrable<T>* t) {
TagType const* const tag = &T::Tag;
void* const pointer = reinterpret_cast<void*>(t);

Get().insert(std::make_pair(tag, pointer));
}

template <typename T>
void Registry::Unregister(Registrable<T>* t) {
TagType const* const tag = &T::Tag;
void* const pointer = reinterpret_cast<void*>(t);

Get().erase(std::make_pair(tag, pointer));
}

Registry::Store& Registry::Get() { static Store S; return S; }

//
// class Registrable
//
template <typename T>
class Registrable {
public:
static Registry::TagType const Tag;

Registrable();
~Registrable();

Registrable(Registrable&&) = default;
Registrable& operator=(Registrable&&) = default;

Registrable(Registrable const&) = default;
Registrable& operator=(Registrable const&) = default;
}; // class Registrable

template <typename T> Registry::TagType const Registrable<T>::Tag;

template <typename T>
Registrable<T>::Registrable() { Registry::Register(this); }

template <typename T>
Registrable<T>::~Registrable() { try { Registry::Register(this); } catch(...) {} }

0

不要使用指针,而是使用句柄

  • 如果内存位于dll侧,请仅传递对象的句柄,而不是指针
  • 如果应用程序分配了对象的内存并将其传递给dll,请保留这些指针的表格,并将它们视为句柄

这适用于单个dll。当然,如果从1.dll通过应用程序传递指针到2.dll,则无论如何都会面临最薄弱的可能性。


0

我有一个带有一些限制的解决方案,使用了RTTI…

如果你的实例都派生自一个虚基类,那么你可以安全地重新解释转换为该基类,然后动态地转换为你的其他类…

class Object
{
   virtual ~Object() {}
};

class A : public Object
{
  static bool IsOfThisClass(void *data)
  { 
     return dynamic_cast<A*>((Object*)data) != 0;
  }
}

如果调用 A::IsOfThisClass(someData),则当 someData 是类 A 的实例时会返回 true。

这并不是一种你想要向用户公开的黑科技,因为它仅在 void* 指向从 Object 派生的类时才有效,但在受控情况下,它可以成为一个有用的构建块。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接