现代C++学习—— final 优化虚函数调用

我们都知道,在C++中,虚函数的调用是比较耗时的一种操作。

首先需要查询虚表,然后跳转,调用。

实际上,影响虚函数速度的关键是不能进行内联优化

这里我们会写一些UB的代码,来查看,是否调用了虚函数。

这里你可能需要严格鸽:C++虚函数表的位置——从内存的角度

我们看一下以下代码输出什么

#include <iostream>
struct A {
public:
  virtual void foo() { std::cout << "A \n"; }
  int x, y;
};
struct B : A{
   void foo() override {std::cout<<"B \n";};
};
struct C : A{
public:
  virtual void foo() { std::cout << "C \n"; }
};
int main() {
  using u64 = unsigned long long;
  A *a = new A{};
  {
    a->foo(); 
  }
  C * c = new C{};
  {
    *(u64 *)a = *(u64 *)c;
    a->foo();
  } 
  {
    A aa  = * a;
    aa.foo();
  }
  {
    A &aa = *a;
    aa.foo();
  }
  return 0;
}

可以思考一下

实际上,输出结果为

A

C

A

C

Compiler Explorer - C++

我们来解释一下

第一次输出A,这个很简单,就是函数调用。

接下来这个句话

*(u64 *)a =*(u64 *)c;

他是把,a的虚表,换成了c的。

接下来再调用foo,就是c的foo了。

所以第二次输出是C

第三次,第四次输出,需要知道,只有指针和引用才有多态,才会去查虚表。

也就是

A aa =* a; aa.foo();

是不需要查虚表,所以输出A,而引用要,但我们改了虚表,所以是C

再看另外一份代码

#include <iostream>
struct A {
public:
  virtual void foo() { std::cout << "A \n"; }
  int x, y;
};
struct B : A{
   void foo() override {std::cout<<"B \n";};
};
struct C : A{
public:
  virtual void foo() { std::cout << "C \n"; }
};
int main() {
  using u64 = unsigned long long;
  A *a = new B{};
  C * c = new C{};
  *(u64 *)a = *(u64 *)c;
  a->foo();
  static_cast<B*>(a)->foo();
  B * b = static_cast<B*>(a);
  b->foo();
  return 0;
}

输出

C

C

C

Compiler Explorer - C++

感觉和上面很像?

但是如果我们稍微改一下,给B加个final

#include <iostream>
struct A {
public:
  virtual void foo() { std::cout << "A \n"; }
  int x, y;
};
struct B final : A{
   void foo() override {std::cout<<"B \n";};
};
struct C : A{
public:
  virtual void foo() { std::cout << "C \n"; }
};
int main() {
  using u64 = unsigned long long;
  A *a = new B{};
  C * c = new C{};
  *(u64 *)a = *(u64 *)c;
  a->foo();
  static_cast<B*>(a)->foo();
  B * b = static_cast<B*>(a);
  b->foo();
  return 0;
}

输出就变成了

C

B

B

Compiler Explorer - C++

为什么呢?

这实际上是一个优化,考虑到虚函数的使用场景

一个类,有子类了,我用父类的指针指向了子类,在编译期不知道调用的是哪个函数。

但是如果用final修饰了呢?

我都没有子类了,所以B * 调用的函数是可以在编译期确定的,也就不需要查虚函数表了。

这种优化,叫做去虚拟化

https://quuxplusone.github.io/blog/2021/02/15/devirtualization/

全部评论
这个去虚拟化有东西,怪不得项目代码的集成都用带着final
点赞 回复 分享
发布于 2023-05-06 17:47 北京

相关推荐

你怎么能在生产环境里直接&nbsp;unwrap()&nbsp;啊?!Rust&nbsp;不是这样用的!你应该先认真设计一个靠谱的错误类型,用&nbsp;thiserror&nbsp;或&nbsp;anyhow&nbsp;包装好上下文信息,然后在每一层调用链里用&nbsp;?&nbsp;把错误优雅地向上传递。遇到可能出现网络抖动、I/O&nbsp;超时、序列化失败这种情况,你要先写好健壮的重试逻辑、退避策略和熔断机制,并且在日志里带上&nbsp;trace&nbsp;id,这样&nbsp;SRE&nbsp;才能在凌晨三点定位问题。然后你要写单元测试,把所有可能失败的路径都测一遍;集成测试里还要模拟网络异常和依赖服务挂掉的情况,确保你的代码不会一言不合就&nbsp;panic。接着你要跑一下&nbsp;clippy,把所有&nbsp;“consider&nbsp;handling&nbsp;the&nbsp;Result&nbsp;instead&nbsp;of&nbsp;unwrapping”&nbsp;的警告都修干净;还要跑&nbsp;rustfmt,让代码风格保持一致。之后你才可以&nbsp;commit&nbsp;然后&nbsp;push。你&nbsp;push&nbsp;上去之后,CI&nbsp;会跑&nbsp;cargo&nbsp;test、cargo&nbsp;check、cargo&nbsp;clippy、cargo&nbsp;fmt&nbsp;-check,还有压力测试确保你的服务在压力下不会因为一个&nbsp;unwrap()&nbsp;就直接把整个服务集群带走。等&nbsp;PR&nbsp;至少经过两位&nbsp;reviewer、三个&nbsp;LGTM,并且&nbsp;SRE&nbsp;点头同意这个改动不会再次导致全球范围的&nbsp;5xx&nbsp;风暴之后,我才会考虑把你的分支&nbsp;merge&nbsp;进去。你怎么上来就直接在关键路径&nbsp;unwrap()?!Rust&nbsp;根本不是这样写的!我拒绝合并!
从夯到拉,评价编程语言
点赞 评论 收藏
分享
评论
10
16
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务