C++对象模型剖析(十)一一Function语义学(三)

Function语义学(三)

今天的内容比较简单,

指向 Member Function 的指针

之前在学习 nonstatic data member 的地址时,得到的地址时该 member 在 class 布局中的 bytes 位置。可以这样认为:它是一个不完整的值,他需要被绑定在某个 class object 的地址上,才能够被存取。

取一个 nonstatic member function 的地址,如果该函数是 nonvirtual,得到的结构是它在内存中真正的地址。然而这个地址也是不完全的。它也需要被绑定在某个 class object 的地址上,才能够通过它调用函数,所有的 nonstatic member function 都需要对象的地址。

我们实战一下即可

#include <iostream>

using namespace std;

class Test {
public:

	void test_1() {
		cout << "test_1()" << endl;
	}

	virtual void test_2()
	{
		cout << "test_2()" << endl;
	}

	virtual void test_3()
	{
		cout << "test_3()" << endl;
	}
};

// 用来打印指针地址的函数
void print(void (Test::* ptr)())
{
	printf("address -> %p \n", ptr);
}

int main(int argc, char** argv)
{
	// 声明一个指针对象
	void (Test:: * ptr_1)() = &Test::test_1;
	void (Test:: * ptr_2)();
	ptr_2 = &Test::test_2;
	void (Test:: * ptr_3)() = &Test::test_3;

	print(ptr_1);
	print(ptr_2);
	print(ptr_3);

	// 调用
	Test test{};
	(test.*ptr_1)();
	// 编译器在底层会将他转换为 (ptr_1)(&test)
	Test* test_ = new Test();
	(test_->*ptr_2)();
	// 这个是virtual function 下面会讲到
}

在这里插入图片描述

  • 支持指向 Virtual Member Function 的指针

    其实通过上面的实战,我们也不难看出,test_1()test_2()都是 virtual member function,它们通过 member function pointer 得到的地址值正好相差 8,这正是一个指针的大小。所以,当面对一个 virtual function 时,其地址是未知的,所能知道的仅仅是 virtual function 在其相关的 virtual table 中的索引值。也就是说,对一个 virtual member function 取其地址,所能获得的只是一个索引。

  • 在多重继承下,指向 Member Functions 的指针

    为了让指向 member function 的指针也能够支持多重继承和虚拟继承,Stroustrup(我也不知道是谁)设计了下面的一个结构体:

    struct __mptr {
        int delta;
        int index;
        union {
            ptrtofunc faddr;
            int 	  v_offset;
        };
    };
    

    index 和 faddr 分别持有 virtual table 索引和 nonvirtual memebr function 地址,当 index 不指向 virtual table 时,会被设为 -1。在这种模型下,

    (ptr->*pmf)();
    // 会变成
    (pmf.index < 0) 
    ?  // non-virtual invocation
    (*pmf.faddr)(ptr)
    : // virtual invocation
    (*ptr->vptr[pmf.index](ptr));
    

    它的缺点也很明显,就是每一次进行调用都会执行上面的操作,检查调用的函数是否为 virtual 或 nonvirtual。

    MIcrosoft 把这项检查拿掉,导入了一个 vcall thunk,在这个策略下,faddr 被指定为要不就是真正的 member function 地址(如果函数是 nonvirtual 的话),要不就是 vcall thunk 的地址。于是 virtual 或 nonvirtual 函数的调用操作透明化,vcall thunk 会选出并调用相关 virtual table 中的适当 slot。

    这个结构体的另一个副作用是:当传递一个不变的指针给 member function 时,它需要产生一个临时对象。

    extern Point3d foo( const Point3d&, Point3d (Point3d::*)());
    void bar( const Point3d& p ) {
        POint3d pt = foo( p, &Point3d::normal);
    }
    // 其中&Point3d::normal的值类似这样
    { 0, -1, 10727147 }
    // 将产生一个临时对象,有明确的初值。
    _mptr temp = { 0, -1, 10727147 };
    foo(p, temp);
    

    看到我们最开始的结构体

    struct __mptr {
        int delta;
        int index;
        union {
            ptrtofunc faddr;
            int 	  v_offset;
        };
    };
    

    delta 字段表示 this 指针的 offset 值,而 v_offset 字段放的是一个 virtual (或多重继承中的第二或后继的)base class 的 vptr。如果 vprt 被编译器放在 class 对象的开头处,这个字段就没有必要了,代价则是C对象兼容性降低,这些字段只在多重继承或虚拟继承的情况下才有必要,有许多编译器在自身内部根据不同的 classes 特性提供了多种指向 member functions 的指针的形式。例如,microsoft就供应了三种:

    1. 一个单一继承实例–持有vcall thunk 地址或是函数地址
    2. 一个多重继承实例–持有faddr和delta两个members
    3. 一个虚拟继承实例–持有4个members

inline function 内联函数

看看下面这段代码

if 0

class Point {
	friend Point operator+(const Point&, const Point&);
private:
	int _x;
	int _y;
};

Point operator+(const Point& lhs, const Point& rhs)
{
	Point new_ptr{};

	new_ptr._x = lhs._x + rhs._x;
	new_ptr._y = lhs._y + lhs._y;
 
	return new_ptr;
}
#else 
// 我们看一种比较好的做法
class Point {
public:
	inline int x();
	inline int y();
	inline auto x(int) -> void;
	inline auto y(int) -> void;
private:
	int _x;
	int _y;
};

auto Point::x() -> int
{
	return _x;
}

auto Point::y() -> int
{
	return _y;
}

auto Point::x(int x) -> void
{
	_x = x;
}

auto Point::y(int y) -> void
{
	_y = y;
}

Point operator+(Point& lhs, Point& rhs)
{
	Point new_ptr{};

	new_ptr.x(lhs.x() + rhs.x());
	new_ptr.y(lhs.y() + rhs.y());

	return new_ptr;
}


int main() {}

#endif

熟悉面向对象的兄弟们应该很容易看出上面两种实现的区别。现在我们就来看看 inline 这个关键字吧

关键字 inline只是一项请求,如果这项请求被接收,编译器就必须认为它可以用一个表达式(expression)合理地将这个函数拓展开来。

在某个层次上,如果编译器拓展一个inline函数的执行成本比一般的函数调用及返回机制所带来的符合低,编译器就会对这个inline函数进行展开。编译器有一套复杂的测试法,通常是用来计算 assigments,function calls,virtual function calls 等操作的次数。每个表达式(expression)种类有一个权值,而inline函数的复杂度取决于这些操作的总和。

一般而言,处理一个 inline 函数,有两个阶段:

  1. 分析函数定义,以决定函数的“intrinsic inline ability”(本质的inline能力)。与编译器本身有关。

  2. 真正的inline函数拓展是在调用的那一点上。这会带来参数的求值操作(evaluation)以及临时性对象的管理。

    // 还是上面的例子,这时候
    // 我们会看见编译器帮我们做了一件
    // 很有意思的事情
    new_ptr.x(lhs.x() + rhs.x());
    // 
    new_ptr.x(lhs._x + rhs._x);
    // 因为直接将inline函数展开并不会带来效率上的提升
    

现在我们看看,inline 函数的形式参数

  • formal arguments 形式参数

    一般而言,面对“会带来副作用的实际参数”,通常需要引入临时性对象。换句话说,如果实际参数是一个常量表达式(constant expression),我们可以在替换之前先完成其求值操作,后继的inline替换,就可以把常量直接绑定上去,如果既不是常量表达式,也不是带有副作用的表达式,那么就直接替换。

    inline int min(int i, int j)
    {
        return i < j ? i : j;
    }
    
    inline int bar()
    {
        int minval;
        int val1 = 1024;
        int val2 = 2048;
        
        minval = min(val1, val2);
        // 参数直接替换
        minval = val1 < val2 ? val1 : val2;
        
        minval = min(1024, 2048);
        // 直接用常量结果
        mminval = 1024;
        
        minval = min( foo(), bar()+1);
    	// 引发参数的副作用,需要导入一个临时性对象,以避免重复取值(multiple evaluation)
        int t1, t2;
        minval = 
            ( t1 = foo() ), (t2 = bar() + 1),
        	t1 < t2 ? t1 : t2;
    }
    
  • 局部变量(local variables)

    一般而言,inline函数中的每一个局部变量都必须被放在函数调用的一个封闭区段中,拥有一个独一无二的名称。如果inline函数以单一表达式(expression)扩展多次,则每次拓展都需要自己的一组局部变量。如果inline函数以分离的多个式子(discrete statements)被拓展多次,那么只需一组局部变量,就可以重复使用。因为它们被放在同一个区段中。

    看个例子吧

    inline int min(int i, int j)
    {
        int minval = i < j ? i : j;
        return minval;
    }
    
    {
        int local_var;
        int minval;
        
        // ...
        minval = min(val1, val2);
    }
    
    // 拓展之后
    {
        int local_var;
        int minval;
        
        // 将 inline 函数的局部变量处以“mangline”操作
        int __min_lv_minval;
        minval =
            ( __min_lv_minval = 
            	val1 < val2 ? val1 : val2 ),
        	__min_lv_minval;
    }
    

    inline 函数中的局部变量,再加上有副作用的参数,可能会导致大量的临时对象产生。特别是如果它以单一表达式(expresssion)被拓展多次的话。

    minval = min( val1, val2 ) + min(foo(), foo() + 1);
    // 拓展
    int __min_lv_minval_00;
    int __min_lv_minval_01;
    
    int t1;
    int t2;
    
    minval = 
        ((__min_lv_minval_01 = 
         val1 < val2 ? val1 : val2),
         __min_lv_minval)
        +
        ((__min_lv_minval_01 = (t1 = foo() ),
         ( t2 = foo() + 1), 
         t1 < t2 ? t1 : t2), 
        __min_lv_minval_01);
    

    需要注意的是,如果一个inline被调用太多次的话,会产生大量的代码,是程序的大小暴涨。

    如果inline中又有inline,可能会使一个表面看起来平凡的inline却因为其连锁复杂度而没办法拓展开来。

       val1 < val2 ? val1 : val2),
       __min_lv_minval)
      +
      ((__min_lv_minval_01 = (t1 = foo() ),
       ( t2 = foo() + 1), 
       t1 < t2 ? t1 : t2), 
      __min_lv_minval_01);

需要注意的是,如果一个inline被调用太多次的话,会产生大量的代码,是程序的大小暴涨。

如果inline中又有inline,可能会使一个表面看起来平凡的inline却因为其连锁复杂度而没办法拓展开来。

  • 20
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值