is_base_of是如何工作的?


118

以下代码如何工作?

typedef char (&yes)[1];
typedef char (&no)[2];

template <typename B, typename D>
struct Host
{
  operator B*() const;
  operator D*();
};

template <typename B, typename D>
struct is_base_of
{
  template <typename T> 
  static yes check(D*, T);
  static no check(B*, int);

  static const bool value = sizeof(check(Host<B,D>(), int())) == sizeof(yes);
};

//Test sample
class Base {};
class Derived : private Base {};

//Expression is true.
int test[is_base_of<Base,Derived>::value && !is_base_of<Derived,Base>::value];
  1. 请注意,这B是私有基础。这是如何运作的?

  2. 注意这operator B*()是常量。它为什么如此重要?

  3. 为什么template<typename T> static yes check(D*, T);优于static yes check(B*, int);

注意:它是的简化版本(删除了宏)boost::is_base_of。这适用于各种编译器。


4
您对于模板参数和真实的类名使用相同的标识符非常令人困惑……
Matthieu M. 2010年

1
@Matthieu M.,我已将自己纠正:)
Kirill V. Lyadvinsky 2010年

2
前一段时间我写了一个替代实现的is_base_ofideone.com/T0C1V它虽然(GCC4.3正常工作)不与旧版本的GCC工作。
Johannes Schaub-litb 2010年

3
好吧,我去散散步。
jokoon 2011年

2
此实现不正确。is_base_of<Base,Base>::value应该是true; 这又回来了false
chengiz

Answers:


109

如果他们有关系

让我们暂时假设它B实际上是的基础D。然后,对于的调用check,两个版本都是可行的,因为Host可以将其转换为D* B*。这是用户定义的转换顺序,分别由13.3.3.1.2from Host<B, D>D*and 描述B*。为了找到可以转换类的转换函数,check根据13.3.1.5/1

D* (Host<B, D>&)

第一个转换函数不是候选函数,因为B*不能转换为D*

对于第二种功能,存在以下候选者:

B* (Host<B, D> const&)
D* (Host<B, D>&)

这些是采用宿主对象的两个转换函数候选。第一个通过const引用获取,第二个则没有。因此,第二个是非常量*this对象(隐含对象参数)的更好匹配,用于第二个函数的13.3.3.2/3b1sb4转换。B*check

如果您删除 const,我们将有以下候选人

B* (Host<B, D>&)
D* (Host<B, D>&)

这将意味着我们无法再按常数选择。在普通的重载解决方案场景中,该调用现在将是模棱两可的,因为通常返回类型将不参与重载解决方案。但是,对于转换功能,有一个后门。如果两个转换函数都同样好,则它们的返回类型根据确定谁是最好的13.3.3/1。因此,如果你想删除的常量,那么第一个将采取,因为B*皈依更好的B*D*B*

现在,哪种用户定义的转换顺序更好?一个用于第二个或第一个检查功能?规则是,仅当用户定义的转换序列使用与相同的转换函数或构造函数时,才能进行比较13.3.3.2/3b2。这是完全正确的情况:两者都使用第二个转换函数。注意,const非常重要,因为它强制编译器采用第二个转换函数。

既然我们可以比较它们-哪个更好?规则是,从转换函数的返回类型到目标类型的更好转换将获胜(再次由13.3.3.2/3b2)。在这种情况下,D*将转换成D*比更好B*。这样,第一个函数被选中,我们就可以识别继承了!

注意,由于我们实际上不需要转换为基类,因此我们可以识别私有继承,因为是否可以从a转换D*为a B*并不取决于继承的形式。4.10/3

如果它们不相关

现在,我们假设它们与继承无关。因此,对于第一个功能,我们有以下候选者

D* (Host<B, D>&) 

第二个,我们现在有了另一套

B* (Host<B, D> const&)

由于如果没有继承关系就无法转换D*B*,因此现在在两个用户定义的转换序列之间没有通用的转换函数!因此,如果不是第一个函数是模板这一事实,我们将是模棱两可的。当存在非模板功能时,模板是第二选择13.3.3/1。因此,我们选择非模板函数(第二个),并且我们认识到Band 之间没有继承D


2
啊! 安德烈亚斯(Andreas)的段落正确,很可惜他没有给出这样的答案:)谢谢您的时间,我希望我可以喜欢它。
Matthieu M.

2
这将永远是我最喜欢的答案...一个问题:您阅读了整个C ++标准还是在C ++委员会工作?恭喜你!
Marco A.

4
@DavidKernin在C ++ committe中工作并不会自动使您知道C ++的工作原理:)因此,您肯定必须阅读标准的一部分,以了解详细信息,而我已经完成了。还没有读完所有内容,所以我绝对不能解决大多数标准库或与线程相关的问题:)
Johannes Schaub-litb 2014年

1
@underscore_d公平地说,规范不禁止std ::特质使用一些编译器魔术,以便标准库实现者可以按自己的意愿使用它们。他们将避免模板杂技,这也有助于加快编译时间和内存使用。即使界面看起来像也是这样std::is_base_of<...>。一切都在幕后。
约翰内斯·绍布

2
当然,一般的库boost::需要在使用它们之前确保它们具有这些内在函数。而且我有一种在没有编译器帮助的情况下实现事物的“挑战接受”心态:)
Johannes Schaub-litb 16/02/28

24

让我们通过查看步骤来弄清楚其工作原理。

sizeof(check(Host<B,D>(), int()))零件开始。编译器可以很快看到这check(...)是一个函数调用表达式,因此需要对进行重载解析check。有两个候选超载可用,template <typename T> yes check(D*, T);no check(B*, int);。如果选择第一个,则得到sizeof(yes),否则sizeof(no)

接下来,让我们看一下重载分辨率。第一个重载是模板实例化check<int> (D*, T=int),第二个重载是check(B*, int)。提供的实际参数是Host<B,D>int()。第二个参数显然不能区分它们。它只是使第一个重载成为模板。稍后我们将了解为什么模板部分具有相关性。

现在查看所需的转换顺序。对于第一个重载,我们有Host<B,D>::operator D*-一个用户定义的转换。第二,过载是棘手的。我们需要一个B *,但可能有两个转换序列。一个是通过Host<B,D>::operator B*() const。如果(且仅当)B和D通过继承关联,则转换序列Host<B,D>::operator D*()+ D*->B*存在。现在假设D确实继承自B。这两个转换序列是Host<B,D> -> Host<B,D> const -> operator B* const -> B*Host<B,D> -> operator D* -> D* -> B*

因此,对于相关的B和D,no check(<Host<B,D>(), int())将是模棱两可的。结果,yes check<int>(D*, int)选择了模板。但是,如果D不从B继承,那么no check(<Host<B,D>(), int())就不是模棱两可的。此时,无法根据最短的转换顺序进行过载解决。但是,在给定相等的转换序列的情况下,重载解析更喜欢非模板函数,即no check(B*, int)

现在,您明白了继承是私有的无关紧要的原因:该关系仅用于no check(Host<B,D>(), int())在访问检查发生之前从重载解析中消除。您还将看到为什么operator B* const必须为const的原因:否则就不需要Host<B,D> -> Host<B,D> const步骤,没有歧义,并且no check(B*, int)总是会被选择。


您的解释不说明存在const。如果您的答案是正确的,则const不需要。但这不是真的。删除const和欺骗将不起作用。
阿列克谢·马里斯托夫

如果没有const,则两个转换序列no check(B*, int)不再是模棱两可的。
MSalters 2010年

如果只留下no check(B*, int),那么对于与BD,就不会有歧义。编译器将毫无疑问地选择operator D*()执行转换,因为它没有常量。相反,这有点相反:如果删除 const,则会引入一些歧义,但这可以通过以下事实解决:operator B*()提供更好的返回类型,而无需B*像指针指针那样进行指针转换D*
Johannes Schaub-litb

的确是关键:在两个不同的转换序列之间存在歧义,以便B*<Host<B,D>()临时变量中获得a 。
MSalters 2010年

这是一个更好的答案。谢谢!因此,据我了解,如果一个功能更好,但模棱两可,那么选择另一个功能?
user1289

4

private位被完全忽略,is_base_of因为在可访问性检查之前发生了过载解析。

您可以简单地验证一下:

class Foo
{
public:
  void bar(int);
private:
  void bar(double);
};

int main(int argc, char* argv[])
{
  Foo foo;
  double d = 0.3;
  foo.bar(d);       // Compiler error, cannot access private member function
}

同样适用于此,B作为私有基础不会阻止检查的进行,只会阻止转换,但我们从不要求实际转换;)


有点。完全不执行基本转换。host任意转化成D*B*在未计算的表达。由于某些原因,在某些条件下D*优于B*
Potatoswatter

我认为答案是在13.3.1.1.2中,但我还没有弄清楚细节:)
Andreas Brinck 2010年

我的回答仅解释了“为什么甚至是私人作品”部分,塞利比泽的回答当然更完整,尽管我急于等待根据情况对完整解决方案进行清晰的解释。
Matthieu M.

2

它可能与部分排序wrt重载解析有关。如果D源自B,则D *比B *更专业。

确切的细节相当复杂。您必须弄清各种重载解决规则的优先级。部分排序是其中之一。转换序列的长度/种类是另一种。最后,如果两个可行的功能被认为是同等好的,则选择非模板而不是功能模板。

我从来不需要查看这些规则如何相互作用。但是,似乎部分排序主导了其他重载解决方案规则。当D不从B派生时,部分排序规则不适用,并且非模板更具吸引力。当D从B派生而来时,部分排序开始执行,并使功能模板更具吸引力-看起来如此。

至于继承是私有的:代码从不要求从D *到B *的转换,而这需要公共继承。


我想是这样的,我记得曾经看过关于Boost档案的广泛讨论,有关is_base_of贡献者实现和确保这一点的循环。
Matthieu M.

The exact details are rather complicated- 这才是重点。请解释。我想知道
阿列克谢·马里斯托夫

@Alexey:好吧,我以为我指出了正确的方向。在这种情况下,请查看各种重载解决方案规则如何相互作用。关于这种重载情况的分辨率,从B派生的D和不是从B派生的D之间的唯一区别是部分排序规则。重载分辨率在C ++标准的§13中进行了描述。您可以免费获取草稿:open-std.org/jtc1/sc22/wg21/docs/papers/2005/n1804.pdf
sellibitze 2010年

重载解决方案跨越该草稿中的16页。我猜想,如果您真的需要了解这种情况下的规则及其之间的相互作用,则应阅读完整的第13.3节。我不希望在这里得到100%正确且符合您标准的答案。
sellibitze 2010年

如果您有兴趣,请参阅我的回答以获取解释。
Johannes Schaub-litb

0

在第二个问题之后,请注意,如果不是const,则用B == D实例化Host会导致格式错误。但是is_base_of的设计使每个类都是其自身的基础,因此转换运算符之一必须是常量。

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.