如何在编译时验证 reinterpret_cast 的有效性
Posted
技术标签:
【中文标题】如何在编译时验证 reinterpret_cast 的有效性【英文标题】:How to verify the validity of reinterpret_cast at compile time 【发布时间】:2011-11-03 10:11:27 【问题描述】:我需要一种方法来验证在编译期间指向另一个类(派生或基类)的指针的向上/向下转换不会更改指针值。也就是说,演员表等价于reinterpret_cast
。
具体来说,场景如下:我有一个Base
类和一个Derived
类(显然是从Base
派生的)。还有一个模板Wrapper
类,它包含一个指向作为模板参数指定的类的指针。
class Base
// ...
;
class Derived
:public Base
// ...
;
template <class T>
class Wrapper
T* m_pObj;
// ...
;
在某些情况下,我有一个Wrapper<Derived>
类型的变量,我想调用一个接收(const)引用ro Wrapper<Base>
的函数。显然这里没有自动转换,Wrapper<Derived>
不是从Wrapper<Base>
派生的。
void SomeFunc(const Wrapper<Base>&);
Wrapper<Derived> myWrapper;
// ...
SomeFunc(myWrapper); // compilation error here
有一些方法可以在标准 C++ 的范围内处理这种情况。比如这样:
Derived* pDerived = myWrapper.Detach();
Wrapper<Base> myBaseWrapper;
myBaseWrapper.Attach(pDerived);
SomeFunc(myBaseWrapper);
myBaseWrapper.Detach();
myWrapper.Attach(pDerived);
但我不喜欢这样。这不仅需要笨拙的语法,而且还会产生额外的代码,因为Wrapper
有一个不平凡的 d'tor(您可能已经猜到了),而且我正在使用异常处理。 OTOH,如果指向Base
和Derived
的指针相同(就像在这个例子中,因为没有多重继承) - 可以将myWrapper
转换为所需的类型并调用SomeFunc
,它会起作用!
因此我将以下内容添加到Wrapper
:
template <class T>
class Wrapper
T* m_pObj;
// ...
typedef T WrappedType;
template <class TT>
TT& DownCast()
const TT::WrappedType* p = m_pObj; // Ensures GuardType indeed inherits from TT::WrappedType
// The following will crash/fail if the cast between the types is not equivalent to reinterpret_cast
ASSERT(PBYTE((WrappedType*)(1)) == PBYTE((TT::WrappedType*)(WrappedType*)(1)));
return (TT&) *this; // brute-force case
template <class TT> operator const Wrapper<TT>& () const
return DownCast<Wrapper<TT> >();
;
Wrapper<Derived> myWrapper;
// ...
// Now the following compiles and works:
SomeFunc(myWrapper);
问题在于,在某些情况下,蛮力强制转换无效。例如在这种情况下:
class Base
// ...
;
class Derived
:public AnotherBase
,public Base
// ...
;
这里指向Base
的指针的值与Derived
不同。因此Wrapper<Derived>
不等于Wrapper<Base>
。
我想检测并阻止这种无效的垂头丧气的尝试。我已经添加了验证(如您所见),但它可以在 run-time 中使用。也就是说,代码会编译并运行,并且在运行时会在调试构建中出现崩溃(或断言失败)。
这很好,但我想在编译时捕捉到它并让构建失败。一种 STATIC_ASSERT。
有没有办法做到这一点?
【问题讨论】:
也许在这里使用static_cast
return (TT&) *this; // brute-force case
而不是 c-cast 可能会有所帮助,c-cast 可能正在执行 reinterpret_cast ,这确实会破坏一切。如果两个类共享继承,编译器应该使用static_cast
使指针指向正确的位置。
@RedX:在这个特定的地方,这相当于static_cast
,因为rwo 类Wrapper<Base>
和Wrapped<Derived>
不相关
【参考方案1】:
简短回答:否。
长答案:
在编译时可用的自省有限,例如,您可以(使用函数重载解析)检测类 B 是否是另一个类 D 的可访问基类。
不过就是这样。
该标准不需要完全自省,尤其是:
您不能列出类的(直接)基类 你无法知道一个类是只有一个还是几个基类 您甚至无法知道基类是否是第一个基类当然,无论如何,存在对象布局或多或少未指定的问题(尽管如果我没记错的话,C++11 增加了区分普通布局和具有虚拟方法的类的能力,这有点帮助在这里!)
使用 Clang 及其 AST 检查功能,我认为您可以编写一个专用的检查器,但这似乎很复杂,当然完全不可移植。
因此,尽管你的主张很大胆 P.S.请不要回复“你为什么要这样做”或“这不符合标准”。我知道这一切是为了什么,我有理由这样做。,你必须调整你的方式。
当然,如果我们能更全面地了解您对本课程的使用情况,我们或许可以集思广益,帮助您找到更好的解决方案。
如何实现类似的系统?
我首先建议一个简单的解决方案:
Wrapper<T>
是所有者类,不可复制,不可转换
WrapperRef<U>
在现有 Wrapper<T>
上实现代理(只要 T*
可转换为 U*
)并提供转换工具。
我们将利用所有要操作的指针都继承自UnkDisposable
这一事实(这是至关重要的信息!)
代码:
namespace details
struct WrapperDeleter
void operator()(UnkDisposable* u) if (u) u->Release();
;
typedef std::unique_ptr<UnkDisposable, WrapperDeleter> WrapperImpl;
template <typename T>
class Wrapper
public:
Wrapper(): _data()
Wrapper(T* t): _data(t)
Wrapper(Wrapper&& right): _data()
using std::swap;
swap(_data, right._data);
Wrapper& operator=(Wrapper&& right)
using std::swap;
swap(_data, right._data);
return *this;
T* Get() const return static_cast<T*>(_data.get());
void Attach(T* t) _data.reset(t);
void Detach() _data.release();
private:
WrapperImpl _data;
; // class Wrapper<T>
现在我们已经奠定了基础,我们可以制作自适应代理了。因为我们只会通过WrapperImpl
操作所有内容,所以我们通过在模板构造函数中通过std::enable_if
和std::is_base_of
检查转换来确保类型安全(以及static_cast<T*>
的意义):
template <typename T>
class WrapperRef
public:
template <typename U>
WrapperRef(Wrapper<U>& w,
std::enable_if_c< std::is_base_of<T, U> >::value* = 0):
_ref(w._data)
// Regular
WrapperRef(WrapperRef&& right): _ref(right._ref)
WrapperRef(WrapperRef const& right): _ref(right._ref)
WrapperRef& operator=(WrapperRef right)
using std::swap;
swap(_ref, right._ref);
return *this;
// template
template <typename U>
WrapperRef(WrapperRef<U>&& right,
std::enable_if_c< std::is_base_of<T, U> >::value* = 0):
_ref(right._ref)
template <typename U>
WrapperRef(WrapperRef<U> const& right,
std::enable_if_c< std::is_base_of<T, U> >::value* = 0):
_ref(right._ref)
T* Get() const return static_cast<T*>(_ref.get());
void Detach() _ref.release();
private:
WrapperImpl& _ref;
; // class WrapperRef<T>
它可能会根据您的需要进行调整,例如,您可以删除复制和移动 WrapperRef
类的功能,以避免它指向不再有效的 Wrapper
的情况。
另一方面,您也可以使用shared_ptr
/weak_ptr
方法来丰富它,以便能够复制和移动包装器并仍然保证可用性(但要注意内存泄漏)。
注意:WrapperRef
故意不提供Attach
方法,这样的方法不能与基类一起使用。否则Apple
和Banana
都派生自Fruit
,您可以通过WrapperRef<Fruit>
附加Banana
,即使原来的Wrapper<T>
是Wrapper<Apple>
...
注意:这很容易,因为有共同的UnkDisposable
基类!这就是我们的共同点 (WrapperImpl
)。
【讨论】:
非常感谢您的回答。我想到了编译时检查,某种可以在编译时计算并受STATIC_ASSERT
宏影响的表达式(希望您熟悉这种技术)。顺便说一句,我使用的表达式 PBYTE((WrappedType*)(1)) == PBYTE((TT::WrappedType*)(WrappedType*)(1))
可能理论上是在编译时计算的。我确信在启用优化的发布版本中,编译器将替换已知结果。但是我使用的编译器(msvc)不允许这样做。
更广泛的图片。我有一个基本接口UnkDisposable
,它有一个virtual Release() = 0;
。 Wrapper
是一个 RAII 包装器,它包装指向此类对象的指针,并确保在必要时调用 Release
。 Wrapper
是一个模板类,它封装了任何继承自 UnkDisposable
的类。类以不同的方式实现UnkDisposable
:一些立即删除对象,而另一些实现引用计数(它们也有AddRef
)。继续...
有一个函数,它接受一些参数,负责启动一个异步操作。如果成功 - 它应该拥有适当分配资源的所有权。有一个接口CompletionHandler
继承自UnkDisposable
,它添加了相关的完成/错误处理方法。该函数将Wrapper<CompletionHandler>&
作为参数。如果一切正常 - 它会将分配的对象从给定的包装器中分离出来并获得它的所有权(将其附加到其内部包装器)。继续...
有一个类MyCompletionHandler
派生自CompletionHandler
。它被分配、初始化并传递给函数。我希望能够做到这一点:Wrapper<MyCompletionHandler> pHandler(new MyCompletionHandler);
,然后将其用作预期 Wrapper<CompletionHandler>
或 Wrapper<UnkDisposable>
的参数
@valdo:有什么理由不能使用更传统的基于析构函数的 RAII(尤其是智能指针),而不是尝试模拟 C#?以上是关于如何在编译时验证 reinterpret_cast 的有效性的主要内容,如果未能解决你的问题,请参考以下文章
如何在java中进行reinterpret_cast以获取像素数组[重复]
如何验证编译时在 msvc 2010 c++ 项目中是不是正确设置了编译器选项?
[C/C++]_[初级]_[static_cast,reinterpret_cast,dynimic_cast的使用场景和区别]