".*&" 运算符是做什么用的?

31

我偶然发现了这个问题,其中有一个使用了奇怪结构的答案:

typedef std::queue<int> Q;
typedef Q::container_type C;

C & get (Q &q)
{
    struct hack : private Q {
        static C & get (Q &q) {
            return q.*&hack::c;
        }
    };
    return hack::get(q);
}

通常情况下,我遵循q可以访问其自身的c成员,该成员由get函数引用。但是,我无法清楚地解释这是什么意思。使用.*&时到底发生了什么,为什么它是允许的?


9
这是两个运算符:.*& - chris
6
我认为这个技巧可能是受到了 https://dev59.com/b3VD5IYBdhLWcg3wI3-L#1065606 的启发。 - chris
1
@chris 这是一个很棒的发现。谢谢你! - πάντα ῥεῖ
1
必须说,这看起来确实比运算符-->更有趣(https://dev59.com/RXI-5IYBdhLWcg3w18d3),而且我不知道为什么会使用这种东西,+1。 - vsoftco
2个回答

30
typedef std::queue<int> Q;

Q 是一个适配 queue 的容器。

typedef Q::container_type C;

CQ 的底层容器,Q 是一个使用 deque<int> 作为容器的对象。

C & get (Q &q) {

get接收一个queue对象并返回一个deque对象。实际上,它返回的是queue所包装的deque对象,这在常规情况下是不可能的。

  struct hack : private Q {

hack 是一种只在函数内部使用的类型。它继承自 Q,仅有一个静态成员函数。从它的名称来看,您可能会怀疑它是一种黑科技。您是正确的。

永远不会实例化任何 hack

    static C & get (Q &q) {

hack::get 的签名与 get 本身相同。实际上,我们将所有 get 的工作委托给这个方法。

      return q.*&hack::c;

这一行需要拆分成多行。我会用更多的行来描述:

      using mem_ptr_t = C Q::*; // aka typedef C Q::*mem_ptr_t;
      mem_ptr_t c_mem_ptr = &hack::c;
      C& ret = q.*c_mem_ptr;
      return ret;

第一行定义了一个指向类型为 C 的字段的成员指针类型,它位于 Q 中。C++11 和 C++03 命名此类型的方式都不美观。
第二行获取了一个指向 Q 中字段 c 的成员指针。通过 C++ 类型系统中的漏洞来实现。&hack::c 逻辑上属于类型 C hack::* -- 即在类型为 hack 的类中,类型为 C 的成员指针。实际上,这就是我们为什么可以在 hack 的静态成员中访问它的原因。但所说的 c 实际上位于 Q 中,因此该表达式在 C++ 中的实际类型是 C Q::*:指向 Q 类型的成员变量的指针。
你无法在 hack 中直接获取此成员指针,&Q::c 是不合法的,但 &hack::c 不是。
您可以将成员指针视为对另一种类型的“类型化偏移量”:&hack::ccQ 中的“偏移量”,同时知道它的类型是 C。现在,这并不是真的,它只是一些不透明的值,告诉编译器如何从 Q 中获取 c -- 但以这种方式考虑有助于理解(在简单情况下可能是这样实现的)。
然后,我们将此成员指针与 Q& 一起使用,从 Q 获取 c。获取成员指针受到 protected 的限制:但使用它则没有!我们使用成员解引用运算符 operator .*,该运算符可以在左边传递类实例,在右边传递成员函数指针或成员变量指针。 instance .* member_ptr 是一个表达式,用于找到 instance 中由 member_ptr “指向”的成员。在原始代码中,所有操作都在一行完成。
instance .* &class_name::member_name

看起来好像有一个操作符.*&

    }
  };

然后我们关闭静态方法和 hack 类,并:

  return hack::get(q);
}

调用它。这种技术可以访问protected状态:如果没有它,protected成员只能在相同实例的子类中访问。使用它,我们可以访问任何实例的protected成员,而不违反标准的任何部分。


2
我注意到您从未给出c_mem_ptr的类型,尽管这对整个技巧至关重要(并且违反了类型理论 - 它应该是C& hack::*)。 - Ben Voigt
2
@BenVoigt:天哪,那是一个微妙的评论。谢谢你的洞察力。我错过了引用答案中的这一点:关键点在于成员指针的类型绑定到实际包含该成员的类,而不是您在获取地址时指定的类。 - jxh
@BenVoigt:不,这是C++的Q::*。 - Jamboree
@Jamboree:这就是Ben的观点。这就是它现在的状态,但不是它应该的样子。 - jxh
@BenVoigt 好的,我突出了类型系统中的漏洞,并包含了明确的类型,并移除了auto。这种给类型命名的方式真是糟糕透顶。 - Yakk - Adam Nevraumont
1
@jxh 另外,我还添加了一两段关于.*&的额外说明。我喜欢逐行解释,因为整个代码片段有点晦涩难懂,理解上下文很重要。我本可以创建一个更简单的示例,但我选择了这种方式。 - Yakk - Adam Nevraumont

8

这是一种黑科技,正如名称所示。

.*将对象放在左边,成员指针放在右边,并解析给定对象的指向成员。 &是引用运算符;&Class::Member返回一个成员指针,它本身不能被解引用,但可以与.*->*操作符一起使用(后者是所有C++运算符中最疯狂的)。因此,obj .* &Class::Memberobj.Member具有完全相同的效果

之所以使用这个更复杂的版本,归结于保护语义中的漏洞;基本上,它允许访问基类对象的protected成员,即使对象与执行此脏技的类不属于同一类型。

就我个人而言,我认为这个技巧过于聪明了。我通常会像这样编写代码:

struct hack : private Q {
    static C & get (Q &q) {
        return static_cast<hack &>(q).c;
    }
};

从技术上讲,这样做的安全性稍微差一些,但不会掩盖正在发生的事情。

通常情况下,我会尽量避免写这样的东西。但今天早些时候我确实这样做了,所以我不能抛石头。


7
我不会称呼 UB 为“稍微不够安全”。 - T.C.
@T.C. 我会这样做,因为在这种情况下,没有编译器会产生意外的结果。唯一需要担心的是未来的兼容性问题,不过很难想象任何编译器会有不同的行为。 - Sneftel
将您的代码粘贴到我的示例代码中,无法编译。 - jxh
你的方法在gcc或clang上无法编译。 - Barry
@Barry是一只流浪的const - Sneftel
显示剩余3条评论

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