当operator&重载时,如何可靠地获取对象的地址?


170

考虑以下程序:

struct ghost
{
    // ghosts like to pretend that they don't exist
    ghost* operator&() const volatile { return 0; }
};

int main()
{
    ghost clyde;
    ghost* clydes_address = &clyde; // darn; that's not clyde's address :'( 
}

我如何获得clyde的地址?

我正在寻找一种适用于所有类型对象的解决方案。C ++ 03解决方案会不错,但我也对C ++ 11解决方案感兴趣。如果可能,让我们避免任何特定于实现的行为。

我知道C ++ 11的std::addressof功能模板,但对在这里使用它不感兴趣:我想了解标准库实现者如何实现此功能模板。


41
@jalf:这种策略是可以接受的,但是既然我已经打了个头脑了,那么我该如何处理他们可恶的代码呢?:-)
James McNellis 2011年

5
@jalf Uhm,有时您需要重载此运算符,并返回一个代理对象。虽然我现在想不出一个例子。
Konrad Rudolph

5
@Konrad:我也是。如果需要,我建议一个更好的选择是重新考虑设计,因为重载该运算符只会导致太多问题。:)
杰夫

2
@Konrad:在大约20年的C ++编程中,我曾经试图重载该运算符。那是那二十年的开始。哦,我没有使它可用。因此,运算符重载FAQ条目显示 “一元运算符的地址永远不应过载”。如果您能提出令人信服的示例来重载此操作符,则下次我们见面时您将免费获得啤酒。(我知道您要离开柏林,所以我可以放心地提供:)
sbi 2011年

5
CComPtr<>CComQIPtr<>有一个超载operator&
Simon Richter

Answers:


102

更新:在C ++ 11中,可以使用std::addressof代替boost::addressof


让我们首先从Boost中复制代码,减去编译器在位周围的工作:

template<class T>
struct addr_impl_ref
{
  T & v_;

  inline addr_impl_ref( T & v ): v_( v ) {}
  inline operator T& () const { return v_; }

private:
  addr_impl_ref & operator=(const addr_impl_ref &);
};

template<class T>
struct addressof_impl
{
  static inline T * f( T & v, long ) {
    return reinterpret_cast<T*>(
        &const_cast<char&>(reinterpret_cast<const volatile char &>(v)));
  }

  static inline T * f( T * v, int ) { return v; }
};

template<class T>
T * addressof( T & v ) {
  return addressof_impl<T>::f( addr_impl_ref<T>( v ), 0 );
}

如果我们传递对函数引用会发生什么?

注意:addressof不能与功能指针一起使用

在C ++中,如果void func();声明了,则func是对不带参数也不返回结果的函数的引用。对该函数的引用可以很容易地转换为指向函数的指针@Konstantin-from:根据13.3.3.2两者T &T *对于函数而言是无法区分的。第一个是身份转换,第二个是功能到指针转换,均具有“完全匹配”等级(13.3.3.1.1表9)。

对函数引用经过addr_impl_ref,对于的选择,重载分辨率中存在歧义f,这要归功于dummy参数0,它是第一个参数,int可以提升为long(Integral Conversion)。

因此,我们仅返回指针。

如果我们通过转换运算符传递类型会怎样?

如果转换运算符产生a,T*那么我们就有一个歧义:对于f(T&,long)第二个参数,需要进行整数提升,而对于第一个参数,则需要f(T*,int)对转换运算符进行调用(感谢@litb)

那就是开始的时刻addr_impl_ref。C++标准要求转换序列最多可以包含一个用户定义的转换。通过包装类型addr_impl_ref并强制使用转换序列,我们可以“禁用”该类型附带的任何转换运算符。

因此,f(T&,long)选择了过载(并执行了积分提升)。

其他类型会发生什么?

因此f(T&,long)选择了重载,因为那里的类型与T*参数不匹配。

注意:从文件中有关Borland兼容性的说明来看,数组不会衰减到指针,而是通过引用传递的。

这种过载会发生什么?

我们希望避免应用operator&到该类型,因为它可能已被重载。

该标准保证reinterpret_cast可以用于这项工作(请参阅@Matteo Italia的答案:5.2.10 / 10)。

Boost使用constvolatile限定符添加了一些优点,以避免编译器警告(并正确使用a const_cast删除它们)。

  • 投放T&char const volatile&
  • 去除constvolatile
  • 申请&接线员取地址
  • 投射回 T*

const/ volatile杂耍有点黑魔法,但它确实简化工作(而不是提供4个重载)。请注意,由于T不合格,如果我们传递ghost const&,则T*ghost const*,因此限定词并没有真正丢失。

编辑:指针重载用于函数指针,我对上面的解释做了一些修改。我仍然不明白为什么有必要

以下ideone输出对此进行了某种程度的总结。


2
“如果我们传递指针会发生什么?” 部分不正确。如果我们将指针传递给某些类型U,则将功能的地址类型T推断为“ U *”,而addr_impl_ref将具有两个重载:“ f(U *&,long)”和“ f(U **, int)',显然将选择第一个。
Konstantin Oznobihin 2011年

@Konstantin:是的,我以为f函数模板是两个重载,而它们是模板类的常规成员函数,感谢您指出。(现在,我只需要弄清楚重载的用途是什么?有任何提示吗?)
Matthieu M.

这是一个很好的解释。我有点想这不只是“传递” char*。谢谢你,马修。
James McNellis 2011年

@James:我从@Konstantin那里得到了很多帮助,只要我犯了一个错误,他就会用棍棒击打我的头:D
Matthieu M.

3
为什么需要解决具有转换功能的类型?与调用任何转换函数相比,它不希望精确匹配T*吗?编辑:现在我明白了。可以,但是随着0争论,它最终会变成十字交叉,因此将是模棱两可的。
Johannes Schaub-litb 2011年

99

使用std::addressof

您可以将其视为在后台执行以下操作:

  1. 重新将该对象解释为引用字符
  2. 取得那个地址(不会调用重载)
  3. 将指针转换回您的类型的指针。

现有的实现(包括Boost.Addressof)确实做到了这一点,只是需要格外注意constvolatile限定。


16
我更喜欢这种解释,而不是所选内容,因为它很容易理解。
雪橇

49

boost::addressof@Luc Danton提供的技巧和实现取决于the的魔力reinterpret_cast;该标准在§5.2.10¶10中明确指出

如果可以使用a 明确将类型为“ pointer to ” 的表达式转换为类型“ pointer to ”,则可以将类型的左值表达式强制T1转换为“ reference to T2”类型。也就是说,参考强制转换与使用内置和运算符进行转换具有相同的效果。结果是一个左值,它指向与源左值相同的对象,但类型不同。T1T2reinterpret_castreinterpret_cast<T&>(x)*reinterpret_cast<T*>(&x)&*

现在,这使我们能够将任意对象引用转换为a char &(如果引用是cv限定的,则具有cv限定),因为任何指针都可以转换为a(可能是cv限定的)char *。现在有了char &,不再需要在对象上重载运算符,并且可以使用内置&运算符获取地址。

boost实现增加了一些步骤来处理cv合格的对象:第一步reinterpret_cast完成了const volatile char &,否则,普通的char &强制转换不适用于const和/或volatile引用(reinterpret_cast无法删除const)。然后,使用constvolatile删除和const_cast,使用接收地址&,并reinterpet_cast完成“正确”类型的最终处理。

const_cast需要删除const/ volatile可能已被添加到非const /易失性的参考,但它做什么,是不是“伤害” const/ volatile放在第一位参考,因为最终reinterpret_cast会重新添加CV-资格,如果它是放在首位(reinterpret_cast无法删除,const但可以添加)。

至于中的其余代码addressof.hpp,似乎大多数是用于解决方法的。在static inline T * f( T * v, int )似乎只需要在Borland编译,但它的出现引入了必要addr_impl_ref的,否则指针类型将通过本次超载被抓。

编辑:各种重载具有不同的功能,请参阅 @Matthieu M.优秀答案

好吧,我也不再确定这一点。我应该进一步研究该代码,但是现在我正在做晚餐:),稍后再看。


Matthieu M.关于将指针传递给addressof的解释不正确。不要用这样的编辑破坏您的好答案:)
Konstantin Oznobihin 2011年

“良好的食欲”,进一步的调查表明,需要重载以引用功能void func(); boost::addressof(func);。但是,删除重载并不会阻止gcc 4.3.4编译代码并产生相同的输出,因此我仍然不明白为什么有必要进行此重载。
Matthieu M.

@Matthieu:看来是gcc中的错误。根据13.3.3.2,T&和T *在功能上是无法区分的。第一个是身份转换,第二个是功能到指针转换,均具有“完全匹配”等级(13.3.3.1.1表9)。因此,必须要有其他参数。
Konstantin Oznobihin 2011年

@Matthieu:刚刚在gcc 4.3.4(ideone.com/2f34P)上进行了尝试,并获得了预期的歧义。您是否尝试过重载成员函数,例如在addressof实现或自由函数模板中?后者(如ideone.com/vjCRs)将由于临时参数推导规则(14.8.2.1/2)而导致选择“ T *”重载。
Konstantin Oznobihin 2011年

2
@curiousguy:您为什么认为应该?正如我所描述的,我已经参考了特定的C ++标准部分,规定了编译器应该做什么以及我可以访问的所有编译器(包括但不限于gcc 4.3.4,comeau-online,VC6.0-VC2010)报告的歧义。您能详细说明一下此案的原因吗?
Konstantin Oznobihin 2011年

11

我已经看到了addressof这样做的实现:

char* start = &reinterpret_cast<char&>(clyde);
ghost* pointer_to_clyde = reinterpret_cast<ghost*>(start);

不要问我这是如何符合要求的!


5
法律。char*是列出的键入别名规则的例外。
小狗

6
@DeadMG我不是说这不符合要求。我是说你不应该问我:)
吕克·丹顿

1
@DeadMG这里没有别名问题。问题是:reinterpret_cast<char*>定义明确。
curiousguy 2011年

2
@curiousguy并且答案是肯定的,始终允许将任何指针类型强制转换为指针类型[unsigned] char *,从而读取指向对象的对象表示形式。这是另一个char具有特殊特权的区域。
underscore_d

@underscore_d只是“始终允许”强制转换并不意味着您可以对强制转换的结果做任何事情。
curiousguy

5

看一下boost :: addressof及其实现。


1
Boost代码虽然很有趣,但是没有说明其技术如何工作(也没有说明为什么需要两个重载)。
James McNellis 2011年

您的意思是“静态内联T * f(T * v,int)”过载吗?看起来仅Borland C解决方法需要它。那里使用的方法非常简单。唯一微妙的(非标准)问题是将“ T&”转换为“ char&”。尽管是标准格式,但允许从“ T *”转换为“ char *”,对于参考转换似乎没有这样的要求。但是,人们可能希望它在大多数编译器上都能完全相同。
Konstantin Oznobihin 2011年

@Konstantin:之所以使用重载,是因为对于指针,它addressof返回指针本身。这是否是用户想要的,这是有争议的,但这是它指定的方式。
Matthieu M.

@Matthieu:确定吗?据我所知,任何类型(包括指针类型)都包装在内addr_impl_ref,因此永远不要调用指针重载……
Matteo Italia

1
@KonstantinOznobihin并不能真正回答问题,因为您所说的只是在哪里寻找答案,而不是答案
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.