5

更新:我很欣赏“不想要那个,想要这个”的建议。它们很有用,尤其是在激励场景的上下文中提供时。仍然......不管是好是坏,我都很好奇找到一个硬性的“是的,可以在 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 的指针”类型的纯右值 v 转换为“指向 cv T2 的指针”类型时,结果为 static_cast(static_cast(v))如果 T1 和 T2 都是标准布局类型 (3.9) 并且 T2 的对齐要求不比 T1 更严格,或者任何一个类型都是无效的。将“指向 T1 的指针”类型的纯右值转换为“指向 T2 的指针”类型(其中 T1 和 T2 是对象类型,并且 T2 的对齐要求不比 T1 的对齐要求更严格)并返回其原始类型会产生原始类型指针值。未指定任何其他此类指针转换的结果。

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

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

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

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

4

4 回答 4

5

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

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

本文总结得很好:http ://dbp-consulting.com/StrictAliasing.pdf

于 2012-07-02T23:15:32.717 回答
3

如果我理解正确,你有:

  • 一个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)源类型和最终类型都是指向标准布局对象的指针的情况下的行为,在这种情况下,标准保证您获得相同的指针d 从转换到 avoid*然后到最终类型。在不调用未定义行为的情况下,您仍然不能将对象用作它创建的类型以外的任何类型。

于 2012-06-27T04:56:36.500 回答
3

访问器一词是一个死的赠品:您正在寻找的是Proxy

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

// 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;
};

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

于 2012-06-27T06:40:21.360 回答
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 中)以及当实际(“动态") 对象类型是已知的,该列表不包括通过对任何派生类型的引用进行的访问。因此,s中对thisto的显式(和有效)向下转换将放在此处语法引用的类型列表中,并且优化器必须将生成的引用视为对 object (其他名称,别名)的潜在合法引用。myPOD*helper()myPODa

于 2012-07-07T19:54:02.883 回答