【问题标题】:reinterpret_cast of a reference to a shared_ptrreinterpret_cast 对 shared_ptr 的引用
【发布时间】:2020-02-02 20:43:34
【问题描述】:

让 B 派生自 A 类。通过阅读各种帖子,我的印象是像 in 一样进行转换

    const std::shared_ptr<const A> a(new B());
    const std::shared_ptr<const B>& b = reinterpret_cast<const std::shared_ptr<const B>&>(a);

出于某种原因不鼓励使用,应该使用 reinterpret_pointer_cast 代替。但是,出于性能原因,我想避免创建新的 shared_ptr 。上面的代码合法吗?它会导致未定义的行为吗?它似乎在 gcc 和 Visual Studio 中工作。

【问题讨论】:

  • 100% 未定义。
  • 为什么要这样做?多态性的全部意义不在于不知道动态类型是什么吗?
  • @curiousguy 这已经是严格意义上的 UB 了吗?强制转换是未指定的行为,我认为当您使用指针时它会得到 UB,因为它违反了严格的别名规则。并不是说这种吹毛求疵真的很重要,但我很好奇。
  • @Jens 不,它是 UB,因为没有什么可以定义使用强制转换的 std 类的结果。
  • @curiousguy 只是投射不是 UB:stackoverflow.com/questions/58329972/…。还有一个对访问对象意味着什么的定义的参考。从正式的角度来看,我认为这个定义并不令人满意。

标签: c++ shared-ptr reinterpret-cast


【解决方案1】:

你想要static_pointer_cast

const std::shared_ptr<const A> a(new B());
const std::shared_ptr<const B> b = std::static_pointer_cast<const B>(a);

我非常怀疑上述内容会导致任何性能问题。但是,如果您有证据表明 shared_ptr 会产生性能问题,则回退到原始指针:

    const B* pB = static_cast<const B*>(a.get());

另一个提示。请尽量避免在具有继承关系的类之间使用reinterpret_cast。在存在虚方法和/或多重继承的情况下,static_cast 将正确地将指针偏移量调整为正确的 vtable 或基本偏移量。但reinterpret_cast 不会。 (或者从技术上讲:未定义的行为

【讨论】:

【解决方案2】:

reinterpret_cast 通常会导致 UB。有时出于性能原因你愿意冒险使用它,但你会尽量避免这种事情。在这种情况下,您最好使用static_pointer_cast

注意,即使你不知道,在这种情况下,你可以使用哪个其他演员表,并且你愿意冒险使用reinterpret_cast,你必须在演员表之后和之前使用一些验证 - 否则您将能够得到很多错误,并花费大量时间。

【讨论】:

    【解决方案3】:

    首先,您创建一个const std::shared_ptr&lt;const A&gt; a 类型的对象a,并使用指向某个类型B 的指针对其进行初始化。这仅在您可以将B* 分配给A* 时才有效,因此应该存在诸如继承之类的关系。忽略这一点,您可以使用 reinterpret_cast 将某种类型的对象转换为对另一种类型的引用:

    类型 T1 的泛左值表达式可以转换为类型“reference to T2”,如果“指向 T1 的指针”类型的表达式可以显式 使用 reinterpret_cast 转换为“指向 T2 的指针”类型 结果引用与源 glvalue 相同的对象,但使用 指定的类型。 [注意:也就是说,对于左值,引用转换 reinterpret_cast(x) 和转换的效果一样 *reinterpret_cast(&x) 带有内置的 & 和 * 运算符(同样适用于 reinterpret_cast(x))。 ——尾注]

    对于指针,reinterpret_cast 归结为转换为void*,然后转换为目标类型:

    对象指针可以显式转换为对象指针 一种不同的类型。72 当对象指针类型的纯右值 v 是 转换为对象指针类型“pointer to cv T”,结果为 static_cast&lt;cv T*&gt;(static_cast&lt;cv void*&gt;(v)).

    两个静态转换的语义定义为:

    “指向 cv1 void 的指针”类型的纯右值可以转换为纯右值 类型为“指向 cv2 T”的指针,其中 T 是对象类型,而 cv2 是 与 cv1 相同的 cv 限定或更高的 cv 限定。这 空指针值转换为空指针值 目的地类型。如果原来的指针值代表地址 内存中一个字节的A和A满足T的对齐要求, 然后得到的指针值表示与 原始指针值,即 A。任何其他此类的结果 未指定指针转换。

    我正在使用的平台具有 16 位或 32 位的近端和远端指针。在这种情况下,shared_ptr&lt;A&gt;shared_ptr&lt;B&gt; 类型的大小和对齐方式不同,然后将一个转换为另一个是未指定的行为。如果对齐匹配,则定义静态转换的结果。

    但是,关于 reinterpret_cast 引用的第一个子句也包含一个注释

    [ Note: That is, for lvalues, a reference
    cast reinterpret_cast<T&>(x) has the same effect as the conversion *reinterpret_cast<T*>(&x) with
    the built-in & and * operators (and similarly for reinterpret_cast<T&&>(x)). —end note ]
    

    所以基本上,转换在语义上与立即取消引用的指针转换相同。即使指针大小相同(并且对齐方式兼容),使用强制转换的指针也会违反严格的别名规则,因为取消引用是一种访问。

    如果程序试图访问对象的存储值 通过不是其中一个的glvalue 以下类型的行为未定义:53 — 对象的动态类型, — 对象动态类型的 cv 限定版本, — 与对象的动态类型类似(如 4.4 中定义)的类型, — 与对象的动态类型相对应的有符号或无符号类型, — 有符号或无符号类型,对应于动态类型的 cv 限定版本 对象的, — 在其元素中包含上述类型之一或非静态的聚合或联合类型 数据成员(递归地包括子聚合的元素或非静态数据成员) 或包含联合),

    【讨论】:

    • 什么是“对象的动态类型”?
    • @curiousguy 静态类型是在编译时推断的表达式的类型,例如声明的变量类型。动态类型是实际对象的类型,即A* a = new B()类型的指针指向静态类型A但动态类型B的对象。
    • 实际的对象,而不是什么,想象的对象? a 真正指向什么? B 对象如何具有静态类型 A
    • @curiousguy 不确定您在问什么以及您想象对象的位置。静态和动态类型是可以轻松搜索的标准术语。我再试一次。假设您有一个class A {public: virtual void foo() {} } 和一个class B: public A {}。没有想象一个函数void bar(A* a) {a-&gt;foo();}。在编译时,您现在才知道a 指向的任何内容都是A 类型。但是,您可以调用bar(new B()) 并且a 指向的对象的实际类型是B,以及B 中的实现将被调用。这是动态类型,运行时被引用对象的类型。
    • @curiousguy 我没有想象任何物体,不确定我的评论中的位置。我有一种感觉你在取笑,所以请更具体地说明静态类型在哪里是无意义的(也许在一个新问题中?)。您能否详细说明有关指向基数的指针的问题? A* 类型的指针a 指向给定地址处的一个对象,该对象的类型为B。这就是重点。 oibject 看起来像是A 类型,即您只能使用A 的接口。这是静态类型。但它实际上是B类型,所以动态类型是B,方法解析为B
    【解决方案4】:

    使用 shared_ptr 或任何其他标准类或模板的功能,仅在为您传递给函数的类型的类调用函数(包括成员函数)时定义(包括隐式 this论点):

    标准中没有任何内容定义了当您调用一个期望 Foo 并传递 Bar 的标准函数时会发生什么,对于任何两种标准类型 FooBar(甚至对于用户类型。)

    这是没有定义的;这是定义的。不满足最基本的前提条件:使用正确类型的参数。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2023-02-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-08-15
      相关资源
      最近更新 更多