`is_base_of` 是如何工作的?

Posted

技术标签:

【中文标题】`is_base_of` 是如何工作的?【英文标题】:How does `is_base_of` work? 【发布时间】:2011-02-24 01:20:01 【问题描述】:

以下代码是如何工作的?

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];

    请注意,B 是私有基础。这是如何运作的?

    请注意,operator B*() 是常量。为什么它很重要?

    为什么 template&lt;typename T&gt; static yes check(D*, T);static yes check(B*, int); 更好?

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

【问题讨论】:

对模板参数和真正的类名使用相同的标识符是非常令人困惑的...... @Matthieu M.,我已经自己纠正了:) 前段时间我写了一个is_base_of的替代实现:ideone.com/T0C1V它不适用于旧的GCC版本(GCC4.3工作正常)。 好的,我要去散步了。 这个实现不正确。 is_base_of&lt;Base,Base&gt;::value 应该是 true;这将返回false 【参考方案1】:

如果他们是相关的

让我们暂时假设B 实际上是D 的基础。然后对于check 的调用,这两个版本都是可行的,因为Host 可以转换为D* B*。这是一个用户定义的转换序列,如13.3.3.1.2 所述,分别从Host&lt;B, D&gt;D*B*。为了找到可以转换类的转换函数,根据13.3.1.5/1为第一个check函数合成以下候选函数

D* (Host<B, D>&)

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

对于第二个函数,存在以下候选:

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

这是两个采用宿主对象的转换函数候选者。第一个通过 const 引用获取它,而第二个没有。因此,第二个更适合 13.3.3.2/3b1sb4 的非 const *this 对象(隐含对象参数),并用于将第二个 check 函数转换为 B*

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

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

这意味着我们不能再通过 constness 进行选择。在普通的重载解决方案中,调用现在将是模棱两可的,因为通常返回类型不会参与重载解决。然而,对于转换函数,有一个后门。如果两个转换函数同样好,那么它们的返回类型根据13.3.3/1决定谁最好。因此,如果您要删除 const,则将采用第一个,因为 B* 转换为 B*D* 转换为 B* 更好。

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

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

请注意,由于我们不需要实际上转换为基类,因此我们可以识别私有继承,因为我们是否可以从D* 转换为@根据4.10/3,987654354@不依赖于继承形式

如果它们不相关

现在让我们假设它们没有继承关系。因此,对于第一个函数,我们有以下候选者

D* (Host<B, D>&) 

第二个我们现在有了另一组

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

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

【讨论】:

啊!安德烈亚斯的段落是对的,可惜他没有给出这样的答案:)谢谢你的时间,我希望我能把它收藏起来。 这将是我最喜欢的答案... 一个问题:您阅读了整个 C++ 标准还是只是在 C++ 委员会工作?恭喜! @DavidKernin 在 C++ 提交中工作不会自动让你知道 C++ 是如何工作的 :) 所以你必须阅读标准中需要了解细节的部分,我已经完成了.还没有读完,所以我对大多数标准库或线程相关的问题肯定无能为力:) @underscore_d 公平地说,规范并没有禁止 std:: 特征使用一些编译器魔法,所以标准库实现者 can use them of they like。他们将避免模板杂技,这也有助于加快编译时间和内存使用。即使 interface 看起来像 std::is_base_of&lt;...&gt; 也是如此。这一切都在幕后。 当然,像boost:: 这样的通用库在使用它们之前需要确保它们具有这些内在函数。而且我觉得他们之间存在某种“接受挑战”的心态,可以在没有编译器帮助的情况下实现事物:)【参考方案2】:

让我们通过查看步骤来了解它是如何工作的。

sizeof(check(Host&lt;B,D&gt;(), int())) 部分开始。编译器可以很快看出这个check(...)是一个函数调用表达式,所以需要对check做重载解析。有两个候选重载可用,template &lt;typename T&gt; yes check(D*, T);no check(B*, int);。如果选择第一个,你会得到sizeof(yes),否则是sizeof(no)

接下来,让我们看看重载决议。第一个重载是模板实例化check&lt;int&gt; (D*, T=int),第二个候选是check(B*, int)。提供的实际参数是Host&lt;B,D&gt;int()。第二个参数显然不能区分它们;它只是使第一个重载成为模板。我们稍后会看到为什么模板部分是相关的。

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

因此,对于相关的 B 和 D,no check(&lt;Host&lt;B,D&gt;(), int()) 会模棱两可。结果,选择了模板化的yes check&lt;int&gt;(D*, int)。但是,如果 D 不是从 B 继承的,那么 no check(&lt;Host&lt;B,D&gt;(), int()) 就没有歧义。此时,无法根据最短转换序列进行过载解决。但是,给定相同的转换序列,重载解析更喜欢非模板函数,即no check(B*, int)

您现在明白为什么继承是私有的并不重要:该关系仅用于在访问检查发生之前从重载决议中消除no check(Host&lt;B,D&gt;(), int())。你也明白为什么operator B* const 必须是 const:否则就不需要Host&lt;B,D&gt; -&gt; Host&lt;B,D&gt; const 步骤,没有歧义,no check(B*, int) 将始终被选中。

【讨论】:

你的解释不能说明const的存在。如果您的答案是正确的,则不需要const。但事实并非如此。删除const 并且技巧将不起作用。 如果没有 const,no check(B*, int) 的两个转换序列不再模棱两可。 如果只留下no check(B*, int),那么对于相关的BD,就不会模棱两可了。编译器会明确选择operator D*() 来执行转换,因为它没有常量。这有点相反:如果你删除 const,你会引入一些模棱两可的感觉,但是operator B*() 提供了一个不需要的高级返回类型这一事实解决了这个问题指针转换为 B* 就像 D* 一样。 这确实是重点:两个不同的转换序列之间的歧义是从&lt;Host&lt;B,D&gt;() 临时获得B* 这是一个更好的答案。谢谢!所以,据我了解,如果一个功能更好,但不明确,那么选择另一个功能?【参考方案3】:

is_base_of 完全忽略了private 位,因为重载决议发生在可访问性检查之前。

您可以简单地验证这一点:

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* 更可取。 我认为答案在 13.3.1.1.2 但我还没有理清细节:) 我的回答只解释了“为什么还要私人工作”部分,sellibitze 的回答肯定更完整,尽管我急切地等待对完整解决过程的明确解释,具体取决于具体情况。【参考方案4】:

这可能与 w.r.t 的部分排序有关。重载决议。在 D 派生自 B 的情况下,D* 比 B* 更专业。

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

我从来不需要查看这些规则是如何相互作用的。但似乎部分排序正在主导其他重载解决规则。当 D 不是从 B 派生时,部分排序规则不适用,并且非模板更具吸引力。当 D 派生自 B 时,部分排序开始发挥作用,使函数模板更具吸引力——正如它看起来的那样。

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

【讨论】:

我认为是这样的,我记得我曾看过关于 is_base_of 实施的 boost 档案的广泛讨论,以及贡献者为确保这一点而经历的循环。 The exact details are rather complicated - 这就是重点。请解释。我很想知道。 @Alexey:嗯,我以为我给你指出了正确的方向。查看在这种情况下各种重载解决规则如何交互。从 B 派生的 D 和不从 B 派生的 D 在这种重载情况的解决方面的唯一区别是偏序规则。 C++ 标准的§13 中描述了重载解决方案。您可以免费获得草稿:open-std.org/jtc1/sc22/wg21/docs/papers/2005/n1804.pdf 重载解决方案在该草稿中跨越 16 页。我想,如果你真的需要了解这种情况下的规则和它们之间的交互,你应该阅读完整的第 13.3 节。我不会指望在这里得到 100% 正确且符合您标准的答案。 如果您有兴趣,请查看我的答案以获得解释。【参考方案5】:

在您的第二个问题之后,请注意,如果它不是用于 const,则如果用 B == D 实例化 Host 将是不正确的。但是 is_base_of 的设计使得每个类都是其自身的基础,因此是其中之一转换运算符必须是 const。

【讨论】:

以上是关于`is_base_of` 是如何工作的?的主要内容,如果未能解决你的问题,请参考以下文章

从技术上讲,可变参数函数是如何工作的? printf 是如何工作的?

什么是Excel的工作表?如何创建新的工作表?

工作用哪个邮箱好用,如何正确使用邮件工作?

如何应对重复性的工作?

如何给EXCEL一个工作薄的一个工作表加密?

DMA 是如何工作的? DMA的工作流程是啥? [关闭]