仅通过指针强制转换使类类型可互换,而无需分配任何新对象

Make interchangeable class types via pointer casting only, without having to allocate any new objects?

本文关键字:可互换 对象 任何新 分配 类型 指针 转换      更新时间:2023-10-16

UPDATE:我很欣赏"不想要那个,想要这个"的建议。 它们很有用,尤其是在激励场景的上下文中提供时。 还。。。无论好坏,我都很好奇地找到一个硬性的"是的,可以在 C++11 中合法完成""不,不可能做这样的事情"。


我想将对象指针"别名"为另一种类型,其唯一目的是添加一些辅助方法。 别名不能将数据成员添加到基础类(事实上,我越能防止这种情况发生越好! 所有别名同样适用于此类型的任何对象...如果类型系统可以提示哪个别名可能最合适,这将很有帮助。

不应有关于在基础对象中编码的任何特定别名的信息。 因此,我觉得你应该能够"欺骗"类型系统,让它成为一个注释......在编译时检查,但最终与运行时强制转换无关。 大致如下:

Node<AccessorFoo>* fooPtr = Node<AccessorFoo>::createViaFactory();
Node<AccessorBar>* barPtr = reinterpret_cast< Node<AccessorBar>* >(fooPtr);

在引擎盖下,工厂方法实际上是创建一个NodeBase类,然后使用类似的reinterpret_cast将其作为Node<AccessorFoo>*返回。

避免这种情况的简单方法是创建这些包装节点并按值传递的轻量级类。 因此,您不需要强制转换,只需要采用节点句柄包装其构造函数的 Accessor 类:

AccessorFoo foo (NodeBase::createViaFactory());
AccessorBar bar (foo.getNode());

但是,如果我不必为所有这些付出代价,我就不想这样做。 例如,这将涉及为每种包装的指针(AccessorFooShared,AccessorFooUnique,AccessorFooWeak等)创建一个特殊的访问器类型。 最好将这些类型化指针设置为一个基于指针的对象标识的别名,并提供很好的正交性。

所以回到最初的问题:

Node<AccessorFoo>* fooPtr = Node<AccessorFoo>::createViaFactory();
Node<AccessorBar>* barPtr = reinterpret_cast< Node<AccessorBar>* >(fooPtr);

似乎有一些方法可以做到这一点,这可能是丑陋的,但不会"违反规则"。 根据ISO14882:2011(e) 5.2.10-7:

对象指针可以显式转换为不同类型的对象指针。70 当类型为"指向 T1 的指针"的 prvalue v 转换为"指向 cv T2 的指针"类型时,如果 T1 和 T2 都是标准布局类型 (3.9),并且 T2 的对齐要求并不比 T1 更严格,则结果为 static_cast(static_cast(v)) 或者,如果任一类型为无效。将类型为"指向 T1 的指针"的 prvalue 转换为"指向 T2 的指针"类型(其中 T1 和 T2 是对象类型,其中 T2 的对齐要求不比 T1 的对齐要求更严格)并返回其原始类型将生成原始指针值。未指定任何其他此类指针转换的结果。

深入研究"标准布局类"的定义,我们发现:

  • 没有非标准布局类(或此类类型的数组)或引用类型的非静态数据成员,并且
  • 没有虚函数 (10.3) 和没有虚拟基类 (10.1),以及
  • 对所有非静态数据成员具有相同的访问控制(第 11 条),并且
  • 没有非标准布局基类,并且
  • 在派生最多的类中没有非静态数据成员,最多有一个具有非静态数据成员的基类,
  • 或者没有具有非静态数据成员的基类,并且
  • 没有与第一个非静态数据成员类型相同的基类。

听起来像使用这样的东西会让我的手有点束缚,因为访问器或节点中没有虚拟方法。 然而,C++11显然std::is_standard_layout保持检查。

这能安全地完成吗? 似乎在 gcc-4.7 中有效,但我想确保我没有调用未定义的行为。

我相信

严格的别名规则禁止您尝试做的事情。

澄清一下:严格的别名与布局兼容性、POD 类型或其他无关。这与优化有关。以及语言明确禁止您做的事情。

这篇论文总结得相当好:http://dbp-consulting.com/StrictAliasing.pdf

术语 Accessor 是一个死赠品:您正在寻找的是代理

没有理由不按值传递代理。

// Let us imagine that NodeBase is now called Node, since there is no inheritance
class AccessorFoo {
public:
    AccessorFoo(Node& n): node(n) {}
    int bar() const { return node->bar; }
private:
    std::reference_wrapper<Node> node;
};

然后你可以自由地从一个访问器转换到另一个访问器......虽然这闻起来。通常,拥有访问器的目标就是以某种方式限制访问,因此将 nily willy 强制给另一个访问器是不好的。但是,可以支持转换为较窄的访问器。

如果我理解正确,你有:

  • 有状态的NodeBase类,是系统的真正主力;
  • 一组无状态Accessor类型,提供NodeBase接口;和
  • 包装访问器的Node<AccessorT>类,大概提供方便的功能。

我假设最后一点,因为如果你没有一个包装器类型来做方便的事情,那么没有理由不把Accessor类型作为你的顶级,就像你建议的那样:传递AccessorFoo并按值AccessorBar。它们不是同一个对象这一事实完全没有意义;如果您将它们视为指针,那么您会注意到&foo != &bar并不比拥有NodeBase* p1 = new NodeBase; NodeBase* p2 = p1;并注意到,当然,&p1 != &p2更有趣。

如果你真的需要一个包装器Node<AccessorT>并且想让它成为标准布局,那么我建议你利用Accessor类型的无状态来发挥自己的优势。如果它们只是一个无状态的功能容器(它们必须是;否则你为什么能够自由地投射它们?),那么你可以做这样的事情:

struct AccessorFoo {
    int getValue(NodeBase* n) { return n->getValueFoo(); }
};
struct AccessorBar {
    int getValue(NodeBase* n) { return n->getValueBar(); }
};
template <typename AccessorT>
class Node {
    NodeBase* m_node;
public:
    int getValue() {
        AccessorT accessor;
        return accessor.getValue(m_node);
    }
};

在这种情况下,您可以添加模板化转换运算符:

template <typename OtherT>
operator Node<OtherT>() {
    return Node<OtherT>(m_node);
}

现在,您可以在您喜欢的任何Node<AccessorT>类型之间进行直接价值转换。

如果你更进一步,你会使Accessor类型的所有方法都是静态的,并到达特征模式。


顺便提一下,您引用的 C++ 标准部分涉及在源类型和最终类型都是指向标准布局对象的指针的情况下reinterpret_cast<T*>(p)的行为,在这种情况下,标准保证您获得的指针与从强制转换到void*再到最终类型获得的指针相同。您仍然无法在不调用未定义行为的情况下将对象用作创建对象的类型以外的任何类型。

struct myPOD {
   int data1;
   // ...
};
struct myPOD_extended1 : myPOD {
   int helper() { (*(myPOD*)this)->data1 = 6; };  // type myPOD referenced here
};
struct myPOD_extended2 : myPOD { 
   int helper() { data1 = 7; };                   // no syntactic ref to myPOD
};
struct myPOD_extended3 : myPOD {
   int helper() { (*(myPOD*)this)->data1 = 8; };  // type myPOD referenced here
};
void doit(myPOD *it)
{
    ((myPOD_extended1*)it)->helper();
    // ((myPOD_extended2*)it)->helper(); // uncomment -> program/compile undefined
    ((myPOD_extended3*)it)->helper();
}
int main(int,char**)
{
    myPOD a; a.data1=5; 
    doit(&a);
    std::cout<< a.data1 << 'n';
    return 0;
}

我相信这可以保证在所有符合C++的编译器中工作,并且必须打印 8。取消注释标记的行,所有投注都将关闭。

优化程序可能会通过检查函数中实际引用的语法类型列表来修剪搜索有效的别名引用,以生成正确的结果所需的语法类型列表(在 3.10p10 中),并且当实际("动态")对象类型已知时,该列表不包括通过引用任何派生类型的访问。因此,在helper() s中显式(和有效)this myPOD*的向下转换myPOD将放在语法上引用的类型列表中,并且优化程序必须将生成的引用视为对对象a的潜在法律引用(其他名称,别名)。