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

5

更新:我很感激“不想要那个,而是想要这个”的建议。它们非常有用,特别是在提供了激励场景的情况下。但是...无论好坏如何,我都变得好奇了,是否存在一种硬性的方法来实现“是的,在C++11中可以合法地做到这一点”“不,不可能做到这样的事情”


我想要将一个对象指针“别名”为另一种类型,唯一的目的是添加一些辅助方法。该别名不能向基础类添加数据成员(实际上,我能够防止发生这种情况就更好!)所有别名都适用于此类型的任何对象...只是如果类型系统能够提示哪个别名最合适,那就更好了。

关于任何特定别名的信息都不应该编码在基础对象中。因此,我觉得你应该能够“欺骗”类型系统,让它成为一个注释...在编译时进行检查,但最终与运行时转换无关。类似下面这样的东西:

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

在幕后,工厂方法实际上制作了一个NodeBase类,然后使用类似的reinterpret_cast将其返回为Node *。为了避免这种情况,简单的方法是创建这些轻量级包装节点的类,并按值传递它们。因此,您不需要转换,只需要访问器类,在构造函数中获取节点句柄进行包装:
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的说法:

对象指针可以显式转换为不同类型的对象指针。当类型为“指向T1的指针”的prvalue v被转换为类型“指向cv T2的指针”时,如果T1和T2都是标准布局类型(3.9),而T2的对齐要求不比T1更严格,或者其中一个类型是void,则结果为static_cast(static_cast(v))。将类型为“指向T1的指针”的prvalue转换为类型为“指向T2的指针”的prvalue(其中T1和T2是对象类型,并且T2的对齐要求不比T1更严格),然后再转换回其原始类型,将产生原始指针值。任何其他这样的指针转换的结果是未指定的。

深入到“标准布局类”的定义中,我们发现:
  • 没有非静态数据成员是非标准布局类(或这些类型的数组)或引用;
  • 没有虚函数(10.3)和没有虚基类(10.1);
  • 所有非静态数据成员的访问控制(第11条)相同;
  • 没有非标准布局基类;
  • 在最派生类中没有非静态数据成员,且至多有一个具有非静态数据成员的基类,或者没有具有非静态数据成员的基类;
  • 没有与第一个非静态数据成员类型相同的基类。
听起来像是使用这样的东西会限制我一些,因为访问器或节点中没有虚方法。但是,C++11显然有std::is_standard_layout来保持事情的检查。
这能安全地完成吗?在gcc-4.7中似乎可以工作,但我想确保我没有调用未定义的行为。

初始语法应为 Node<AccessorFoo>& foo = *new Node<AccessorFoo>;。不过我还是不理解问题 :) - iammilind
@iammilind 糟糕,打错字了...已经修改了,谢谢!原则上它可能与这个问题非常相似: https://dev59.com/jUzSa4cB1Zd3GeqPoarx,只是我对所涉及的类型有更多的控制,并且可以从头开始重写一切。因此,我想知道在这种特定情况下可能为我打开哪些选项。 - HostileFork says dont trust SE
我想要将一个对象指针“别名”为另一种类型,只是为了添加一些辅助方法。 为什么要这样做呢?成员函数能给你带来什么自由函数不能提供的?除非我漏掉了一些明显的东西(很有可能),否则这听起来有点误导性的面向对象编程狂热主义... - ildjarn
@ildjarn 或许吧?我曾经感叹过,SO 经常是一个“你解释就完了”(“具体问题在哪里,你的实际问题是什么?”)和“你不解释也完了”(“你为什么需要那个?”)的环境。:( 我有一个最终类,其中有 NC 个 const 操作和 N 个非 const 操作,存储在内存映射文件中。有时我想增强和“类型化”这个类的接口,以帮助人们理解。DOM 是我唯一能想到的最佳类比 - HostileFork says dont trust SE
4个回答

5
我相信严格的别名规则禁止您尝试做的事情。
澄清一下:严格的别名规则与布局兼容性、POD类型或其他无关。它与优化有关,以及语言明确禁止您做的事情。
这篇论文总结得相当好:http://dbp-consulting.com/StrictAliasing.pdf

我发现了一些有用的标签(例如“type-punning”),并从严格别名中找到了一些跳板。但是我发现大多数严格别名的链接似乎都是讨论具有不同大小或布局的类型,而这里只有方法不同,并且没有虚拟派发,因为它们都作为相同类型构造。看来在严格别名中,甚至可以将带符号的类型强制转换为无符号类型……所以我认为这个方法分派的问题取决于类似“标准布局”定义之类的东西(如果有的话),或者其他某个总体规则吗? - HostileFork says dont trust SE
有趣的,感谢更新PDF,我已经阅读过并了解你所说的内容。不过现在我想知道......如果所有类方法都是“const”,那么在其生命周期内永远不会修改数据成员......那会不会合法?(如果不合法,为什么?) - HostileFork says dont trust SE
1
@HostileFork:这是不可能的,因为编译器不需要保证在执行对象的const方法时,它不会通过其其他类型在你脚下发生变化。 - Matthieu M.
@MatthieuM。鉴于jthill的回答,是否存在一种“安全转换”,可以在每次访问之前对基本类型进行绕过别名问题,如果严格遵守? - HostileFork says dont trust SE
4
@HostileFork:老实说?我不知道。而且我甚至不认为有必要去了解这个问题。C++标准非常复杂难以理解,而我目前没有时间去检查所有看起来有问题的答案。因此,我只想说:当你越接近规范的边缘时,就越有可能遇到编译器错误。所以,作为一名工程师,我建议要务实,避免与编译器玩心理游戏;无论你是对还是错都没有意义:当你在生产代码中遇到崩溃时,会受到损失,而不是编译器的开发人员。 - Matthieu M.
我将授予此答案赏金,因为它揭示了我之前不知道的“严格别名”,这似乎是一个基本重要的问题,需要担心,这也是技术上禁止的原因。仍然存在一些灰色地带,即是否“安全转换”足以解决此问题(参见jthill的答案),尽管在大多数合理情况下,这开始变得过于脆弱而难以应用。 - HostileFork says dont trust SE

3
如果我理解正确,您有:
- 一个有状态的NodeBase类,是系统的核心; - 一组无状态的Accessor类型,提供对NodeBase的接口;以及 - 一个Node类,它包装了一个访问器,可能提供方便的函数。
我假设最后一点是因为如果您没有一个执行便利操作的包装类型,那么没有理由不将Accessor类型作为顶层类型,就像您建议的那样:通过值传递AccessorFoo和AccessorBar。它们不是同一个对象这个事实完全无关紧要;如果您把它们看作指针,那么您会注意到&foo != &bar与NodeBase* p1 = new NodeBase; NodeBase* p2 = p1;并注意到当然,&p1 != &p2一样无足轻重。
如果您确实需要一个包装器Node并希望使其标准布局,那么我建议您利用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 类型的方法都设为静态的,并采用 traits pattern,效果会更好。
你引用的C++标准部分涉及 reinterpret_cast<T*>(p) 的行为,假设源类型和最终类型都是指向标准布局对象的指针。在这种情况下,标准保证您得到与从 void* 转换然后转换到最终类型时相同的指针。但是,如果不调用未定义的行为,则无法将对象用作创建它的类型以外的任何类型。

+1 没错,你理解得很好,访问器方法应该是静态的...虽然这对于 Accessor 的作者来说比从 NodeBase 继承并调用 NodeBase 方法要困难。 :-/ 我明确希望这些 Node<X> 类型不要与“工作马” NodeBase 实例分开,因为我使用 unique_ptr<Node<X>> 来跟踪节点的所有权。为了继续使用它,我必须在这些 Node<X> 句柄上添加另一层堆分配跟踪,而不是按值传递。所以我最感兴趣的是标准布局技巧。 - HostileFork says dont trust SE
1
@HostileFork 为什么不将 unique_ptr 放在 Node<X> 内部呢?如果您需要其他类型的智能指针,很容易制作不同的 Node<X> 变体,特别是如果它是一个围绕访问器的小包装器。此外,仔细查看,我认为标准布局技巧不是您想象的那样。引用的部分是关于 reinterpret_cast 在标准布局类型的情况下与 static_cast 共享语义的问题。您仍然不能更改类型而不调用未定义的行为。 - John Calsbeek
我没有注意到那一点...嗯...实际上,节点是通过工厂方法作为NodeBase创建的,然后在返回时向下转换...这样做是否合法或者是否会改变什么?至于创建像UniqueNode<X>这样的轻量级类,它们与Node<X>不同...听起来很有可能,很多事情都是可能的!我只是试图将其打造成一个干净的关注点分离,不浪费时间处理不必要的类组合爆炸,并且尽可能高效。如果我没有找到这个激励,我可能会使用另一种语言。 :) - HostileFork says dont trust SE
似乎即使使用标准布局类型,这也被严格别名要求所禁止(请参见@PaulGroke的回答)。在这种情况下,标准允许您做的唯一事情是暂时将指针保持为另一种类型,然后再转换回来,但在此期间不能使用它?(更具体地说,不能调用任何非const方法?) - HostileFork says dont trust SE
在这个问题上跑了一段时间后,这是我相信我要使用的:Gist上的模式示例库和测试程序。在这种情况下,“Foo”是节点,而“Wrapper”可能会被我称为NodeRef或类似的东西。为了实现效果与“类型punning”相比,它仍然似乎有点绕路,但如果它消除了未定义的行为,并且客户端代码看起来还不错,那么我想这没问题...! - HostileFork says dont trust SE

3
术语“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;
};

然后,您可以自由地从一个访问器转换为另一个访问器...尽管这样做看起来不太好。通常,拥有访问器的目的是以某种方式限制访问,因此随意将其强制转换为另一个访问器是不好的。但是,可以支持向更窄的访问器进行强制转换。

@HostileFork:名称总是有问题的。 Facade 通常更多地是接口的聚合(例如, libclang 是 Clang 库上的 C-Facade)。 Proxy 提供相同接口的想法相当狭隘。根据维基百科,Proxy 为另一个对象提供一个占位符以控制访问、降低成本和降低复杂性。您的问题看起来很像 控制访问,在这种情况下,呈现受限界面是很自然的。 - Matthieu M.
1
@HostileFork:但是Proxy并不是一个Node,因为它的目的是限制可用的接口。对吗? - Matthieu M.
在这里使用reference_wrapper(一个被糖衣包装的指针)是否有任何理由,而实际上使用引用成员也可以很好地工作?除非我漏掉了什么,否则对我来说似乎是不必要的复杂化。 - underscore_d
@underscore_d:让赋值工作起来。您不能将新引用分配给现有引用(也就是重新设置引用),它只会分配所引用的 - Matthieu M.
啊,当然,这就是我所缺少的。我忘记了reference_wrapper的这个好处,因为我从来没有需要使用它并且往往会忘记更细节的内容。谢谢! - underscore_d
显示剩余9条评论

2
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()中显式(有效)地将this向下转换为myPOD*,将myPOD放在那里语法引用类型的列表中,优化器必须将生成的引用视为对对象a的潜在合法引用(其他名称或别名)。


其他人提出的别名参数似乎表明编译器可以自由地假设除了分配类时使用的类型(在这种情况下为myPOD)之外,别名不会存在于其他类型中。因此,data1可能会在其中一个别名中更改,然后该更改不会反映在另一个别名中。@MatthieuM已经建议即使是常量也不能保护您,因为其中一个别名可能是非常量的。您能引用一些理由,说明您认为这将是规则的例外吗? - HostileFork says dont trust SE
你可以将 structA* 转换为一个(甚至完全不相关的)structB*,然后再转回来,编译器必须允许这种可能性:转换指针并将其传递并不意味着转换后的值永远不能用于修改原始对象。 - jthill
看起来你所说的与strict aliasing paper相违背,该论文建议只有在实际分配某个东西的类型之间进行转换时(例如,如果您将某些内容声明为myPOD_extended1,然后将其转换为myPOD并返回),才能这样做。这是我提出的问题的核心论点,它确实听起来像是strict aliasing允许编译器进行优化假设,这将在其他情况下削弱它。 :-/ - HostileFork says dont trust SE
1
一旦编译器看到 doit(&a),就不能假定 a 不会通过该指针或任何副本被访问。在 doit 中的向上转型是明确允许的,参见 5.4p4,并且 helper() 函数接收了该向上转型指针的副本(作为 this),因此编译器也无法假设它们不通过该副本引用 myPOD。我已经为 helper 函数添加了安全转换;我认为编译器必须费尽心思才能处理好这种情况,但我发现以前它并不严格正确(在这个讨论中意味着“是错误的”)。我相信现在是严格正确的。 - jthill
简而言之:是的。详细解释:我现在可以看到,优化器会通过检查函数中实际引用的语法类型列表来修剪对有效别名引用的搜索,与(在3.10p10中)为了产生正确结果所需的语法类型列表进行比较 - 当已知实际(“动态”)对象类型时,该列表不包括通过对任何派生类型的引用进行访问。很好。因此,明确地向下转换“this”将在“helper()”中列出引用的类型,因此,如果优化器无法证明“this!=&a”,则必须将它们视为(有效的)别名。 - jthill
显示剩余4条评论

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