草庐IT

C++对象模型:g++的实现(六)

lycpp 2023-04-15 原文

这篇博客开始介绍《深度探索C++对象模型》第四章的剩余部分,包括成员函数指针和内联函数。

1. 成员函数指针

对于静态成员函数,其和常规的函数是一样的,故这里不做介绍。下面主要介绍非静态的成员函数指针,包括普通的非virtual成员函数指针和virtual成员函数指针。
注意,这篇是按照《深度探索C++对象模型》的内容写的,最后讲到支持多继承的成员函数指针时才会给出真正的成员函数指针的实现!

1.1 非virtual成员函数指针

对于一个非virtual的成员函数取址,得到的就是该成员函数在内存中的地址,但是它不能单独调用,需要使用其绑定的对象/指针/引用调用。

// test26.cpp

class Test {
public:
    Test(int i)
        : m_i(i) 
    {}

    int getInt() const {
        return m_i;
    }

    void setInt(int i) {
        m_i = i;
    }

private:
    int m_i;
};

int main() {
    Test t(1);
    int i = t.getInt();
    void (Test::*pMemberFunc)(int) = nullptr;   // 成员函数指针
    pMemberFunc = &Test::setInt;
    (t.*pMemberFunc)(2);
    i = t.getInt();
}

1.2 支持“指向虚成员函数”的指针

对于非虚成员函数我们可以直接拿到其地址,因为其没有多态性。但对于虚函数,其地址要在运行时确定,因此对于虚成员函数我们取的应该是其相对虚表指针的偏移index。
所以如果有如下类:

class Point {
public:
    Point(int x, int y);
    virtual
    ~Point();

    int x() const {return m_x;}
    int y() const {return m_y;}
    virtual
    int z() const { return 0; }
private:
    int m_x;
    int m_y;
};

对于析构函数取值&Point::~Point取得的是0。
对于x()和y()取址&Point::x, &Point::y得到的是其地址,因为他们不是虚函数。
对于z()取址&Point::z得到的是1。通过pMemberFunc调用z(),其会是类似下面的形式:

(*ptr->vptr[(int)pMemberFunc])(ptr)

1.3 支持多继承的成员函数的指针

在多继承的情况下还要考虑虚函数表的位置问题,因为在多重继承下可能有多个虚函数表;还有this指针可能需要进行偏移,如果派生类没有覆盖第二个或后面的基类的虚函数的话。
为了要支持以上种种特性:如果是非虚函数,指针中要包括其地址;如果是虚函数,要包括其相对虚表指针的偏移;如果是多重继承,还要找到虚函数在哪个虚表中和对this指针进行偏移。
在《深度探索C++对象模型》中提出的是这样的结构:

struct _mptr{
    int delta;
    int index;
    union {
        PtrToFunc faddr;
        int v_offset;
    };
};

其中delta是this指针要进行的偏移,index是虚函数在虚表指针指向空间中的下标,faddr是非虚函数的地址,v_offset是虚表指针的的位置。
所以下面的操作:

(ptr->*pmf)();

会变成:

// 我觉得这个可能是有问题
pmf.index < 0
    ? // 非虚函数调用
    (*pmf.faddr)(paddr)
    : // 虚函数调用
    (*ptr->vptr[pmf.index])(ptr)

《深度探索C++对象模型》中是这么写的,但按照作者的说法,实际的代码应该是:

pmf.index < 0
    ? 
    (pmf.faddr)(pmf + delta)
    : 
    (((vptr*)(ptr+pmf.v_offset))[pmf.index])(ptr+delta)
    // (ptr+pmf.v_offset) 是虚表地址
    // ((vptr*)(ptr+pmf.v_offset))[pmf.index] 是虚表的第pmf.index项
    // ptr+delta是对this指针进行偏移

让我们来看看g++中是怎么实现的:

// test27.cpp

class Point {
public:
    Point(int x, int y);
    virtual
    ~Point();

    int x() const {return m_x;}
    int y() const {return m_y;}
    virtual
    int z() const { return 0; }
private:
    int m_x;
    int m_y;
};

Point::Point(int x, int y)
    : m_x(x), m_y(y)
{}

Point::~Point() {
    m_x = m_y = 0;
}

int main() {
    Point p(1, 2);

    using MemberFunction_t = int (Point::*)() const ;

    MemberFunction_t pVirtualMemberFunc = nullptr;
    MemberFunction_t pMemberFunc = nullptr;

    pMemberFunc = &Point::x;
    pVirtualMemberFunc = &Point::z;
    
    int x = (p.*pMemberFunc)();
    int z = (p.*pVirtualMemberFunc)();
    
    ++z;
}

我们使用gdb看一下这个成员函数指针的size:

(gdb) p sizeof(MemberFunction_t)
$1 = 16

在赋值之后,查看pMemberFunc和pVirtualMemberFunc的二进制是什么:

(gdb) x/2ag &pMemberFunc
0x7ffffffee0d0: 0x8000a86 <Point::x() const>    0x0
(gdb) x/2ag &pVirtualMemberFunc
0x7ffffffee0c0: 0x11    0x0

可以看到g++实现的成员函数指针有两个QWORD(QWORD是size为8字节的【有符号或无符号】整型值)。如果函数指针指向的是非虚函数,第一个QWORD里面是该函数的地址;如果是的话,看上去是该虚函数相对于虚表的偏移+1,因为Point::z在vptr[2]的地方(vptr[0]是Point::~Point,但不调用::operator deletevptr[1]也是Point::~Point,会随后调用::operator delete),那偏移就是0x10,但内容是0x11,可能就是加了1。
让我们看一下汇编代码是怎么操作的:

上面的汇编是即将执行int x = (p.*pMemberFunc)();这一语句。
总结如下:

  • 如果不是虚函数,低8个字节是函数的地址,高8个字节是this指针的偏移;
  • 如果是虚函数,低8个字节是虚表指针相对于this指针的偏移&1(位与操作),而高8个字节同样是this指针的偏移;

这两种情况就按低8个字节的QWORD的最低位是不是1决定:如果是1则是虚成员函数指针,不是1则是非虚成员函数指针。
虚函数地址相对于vptr偏移的字节数肯定是指针大小的整数倍,一般为4或8字节,最后一位肯定是0,所以与一个1可以理解,用的时候只需要减去这一位即可。
但函数地址最后一位肯定是0吗?我就这个问题查阅了资料,在博客《C++语言学习(十四)——C++类成员函数调用分析》中提到:

一般来说因为对齐的关系,函数地址都至少是4字节对齐的。即函数地址的最低位两个bit总是0。

虽然和我的观察略微有不同(在我编译的程序里,Point::x的地址是0x8000a86,只有最后一位是0,倒数第二位是1),但也说明了函数地址确实是有对齐这一现象的。
这里再继续引用一下这篇博客里的论述,用以辅助读者理解(感我写得不如这篇博客远矣):

GCC对于成员函数指针统一使用下面的结构进行表示:

struct        
{    
    void* __pfn;  //函数地址,或者是虚拟函数的index    
    long __delta; // offset, 用来进行this指针调整   
};

不管是普通成员函数,还是虚成员函数,信息都记录在__pfn。一般来说因为对齐的关系,函数地址都至少是4字节对齐的。即函数地址的最低位两个bit总是0。 GCC充分利用了这两个bit。如果是普通的函数,__pfn记录函数的真实地址,最低位两个bit就是全0,如果是虚成员函数,最后两个bit不是0,剩下的30bit就是虚成员函数在函数表中的索引值。
// 注意,在我的版本里(g++ (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0),检查的是随后一位,函数地址也只是2对齐,而不是4对齐
GCC先取出函数地址最低位两个bit看看是不是0,若是0就使用地址直接进行函数调用。若不是0,就取出前面30位包含的虚函数索引,通过计算得到真正的函数地址,再进行函数调用。

这篇博客里还介绍了MSVC对于成员函数指针的实现,使用了thunk技术,大家可以去看一下。(其实这个在《深度探索C++对象模型》,里也有提到,大家感兴趣也可以看看原书)。

2. 内联函数

关于这一部分只是做一个总结,我也不知道如何比较好得验证其中的内容。
关键词inline只是一个请求,一般而言,处理一个inline函数会有两个阶段:

  • 分析函数定义,以解决函数的"intrinsic inline ability"(本质的inline能力)。"intrinsic"(本质的、固有的)一词在这里意指“与编译器相关”【书中原话】

说白了就是编译器要看看能不能内联,要是太复杂就直接编译成函数,(在理想情况下)链接器会把生成的重复的内联函数清理掉。strip命令也可以达成这个目的。

  • 真正的inline函数扩展操作是在调用的那一点上,这会带来参数的求值操作和临时对象的管理。

所谓求值操作是和宏函数做对比的,宏函数只是简单的复制粘贴,但inline函数在调用前会对传参进行求值(无论其内联展开与否)。
比如:

inline
int min(int i, int j) {
    return i < j ? i : j;
}

对于minval = min(foo(), bar() + 1)会扩展成:

int t1, t2;
minval = (t1 = foo()), (t2 = bar() + 1),
        t1 < t2 ? t1 : t2;
// 逗号操作符,
// 从左到右计算,表达式结果为最后一个值。
// 比如 t = foo(), bar();
// 会先调用foo(), 再调用bar(),t的值为bar()的返回值

这种特性使得内联函数比宏函数安全得多。
而临时对象管理则是在函数内联时会产生很多临时变量,比如形参列表、内联函数中的局部变量等等。


其他比如成员函数指针的执行效率我就不多做测试了,这一章也就结束了。
关于后面的内容,我会在有时间的时候做简要的总结,不会像这两章这么详细得分析汇编了,因为我觉得对象布局和虚函数的实现就是书最主要的内容了。
好的,就这样了。

有关C++对象模型:g++的实现(六)的更多相关文章

  1. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

    总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

  2. ruby-on-rails - Rails - 子类化模型的设计模式是什么? - 2

    我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co

  3. ruby-on-rails - 按天对 Mongoid 对象进行分组 - 2

    在控制台中反复尝试之后,我想到了这种方法,可以按发生日期对类似activerecord的(Mongoid)对象进行分组。我不确定这是完成此任务的最佳方法,但它确实有效。有没有人有更好的建议,或者这是一个很好的方法?#eventsisanarrayofactiverecord-likeobjectsthatincludeatimeattributeevents.map{|event|#converteventsarrayintoanarrayofhasheswiththedayofthemonthandtheevent{:number=>event.time.day,:event=>ev

  4. ruby-on-rails - Rails - 一个 View 中的多个模型 - 2

    我需要从一个View访问多个模型。以前,我的links_controller仅用于提供以不同方式排序的链接资源。现在我想包括一个部分(我假设)显示按分数排序的顶级用户(@users=User.all.sort_by(&:score))我知道我可以将此代码插入每个链接操作并从View访问它,但这似乎不是“ruby方式”,我将需要在不久的将来访问更多模型。这可能会变得很脏,是否有针对这种情况的任何技术?注意事项:我认为我的应用程序正朝着单一格式和动态页面内容的方向发展,本质上是一个典型的网络应用程序。我知道before_filter但考虑到我希望应用程序进入的方向,这似乎很麻烦。最终从任何

  5. ruby-on-rails - 在混合/模块中覆盖模型的属性访问器 - 2

    我有一个包含模块的模型。我想在模块中覆盖模型的访问器方法。例如:classBlah这显然行不通。有什么想法可以实现吗? 最佳答案 您的代码看起来是正确的。我们正在毫无困难地使用这个确切的模式。如果我没记错的话,Rails使用#method_missing作为属性setter,因此您的模块将优先,阻止ActiveRecord的setter。如果您正在使用ActiveSupport::Concern(参见thisblogpost),那么您的实例方法需要进入一个特殊的模块:classBlah

  6. ruby-on-rails - 如何验证非模型(甚至非对象)字段 - 2

    我有一个表单,其中有很多字段取自数组(而不是模型或对象)。我如何验证这些字段的存在?solve_problem_pathdo|f|%>... 最佳答案 创建一个简单的类来包装请求参数并使用ActiveModel::Validations。#definedsomewhere,atthesimplest:require'ostruct'classSolvetrue#youcouldevencheckthesolutionwithavalidatorvalidatedoerrors.add(:base,"WRONG!!!")unlesss

  7. ruby-on-rails - form_for 中不在模型中的自定义字段 - 2

    我想向我的Controller传递一个参数,它是一个简单的复选框,但我不知道如何在模型的form_for中引入它,这是我的观点:{:id=>'go_finance'}do|f|%>Transferirde:para:Entrada:"input",:placeholder=>"Quantofoiganho?"%>Saída:"output",:placeholder=>"Quantofoigasto?"%>Nota:我想做一个额外的复选框,但我该怎么做,模型中没有一个对象,而是一个要检查的对象,以便在Controller中创建一个ifelse,如果没有检查,请帮助我,非常感谢,谢谢

  8. Ruby 写入和读取对象到文件 - 2

    好的,所以我的目标是轻松地将一些数据保存到磁盘以备后用。您如何简单地写入然后读取一个对象?所以如果我有一个简单的类classCattr_accessor:a,:bdefinitialize(a,b)@a,@b=a,bendend所以如果我从中非常快地制作一个objobj=C.new("foo","bar")#justgaveitsomerandomvalues然后我可以把它变成一个kindaidstring=obj.to_s#whichreturns""我终于可以将此字符串打印到文件或其他内容中。我的问题是,我该如何再次将这个id变回一个对象?我知道我可以自己挑选信息并制作一个接受该信

  9. ruby-on-rails - 如果 Object::try 被发送到一个 nil 对象,为什么它会起作用? - 2

    如果您尝试在Ruby中的nil对象上调用方法,则会出现NoMethodError异常并显示消息:"undefinedmethod‘...’fornil:NilClass"然而,有一个tryRails中的方法,如果它被发送到一个nil对象,它只返回nil:require'rubygems'require'active_support/all'nil.try(:nonexisting_method)#noNoMethodErrorexceptionanymore那么try如何在内部工作以防止该异常? 最佳答案 像Ruby中的所有其他对象

  10. ruby-on-rails - 未在 Ruby 中初始化的对象 - 2

    我在Rails工作并有以下类(class):classPlayer当我运行时bundleexecrailsconsole然后尝试:a=Player.new("me",5.0,"UCLA")我回来了:=>#我不知道为什么Player对象不会在这里初始化。关于可能导致此问题的操作/解释的任何建议?谢谢,马里奥格 最佳答案 havenoideawhythePlayerobjectwouldn'tbeinitializedhere它没有初始化很简单,因为你还没有初始化它!您已经覆盖了ActiveRecord::Base初始化方法,但您没有调

随机推荐