话说Java里有个很强大的关键字叫synchronized,可以方便的实现线程同步。今天异想天开,尝试在C++里模拟一个类似的。
最近在学习C++的STL,看见智能指针这章节时,无不感叹利用语言的丰富特征,来各种实现各种巧妙的构思。最经典的莫过于使用栈对象构造/析构函数,来维护局部资源的初始化和释放。照着这个巧妙的方法,依样画葫芦自己也来写一个,来实现局部代码线程同步。
Java里的synchronized有两种形式,一种是基于函数的,另种则是语块的。前者受C++的语法所限,估计是没法实现了,所以就尝试后者。
块级语法很简单:
// code
}
因为Java所有变量都继承于Object,所以任意变量都能当作锁用。这在C++里无法简易实现,因此我们用特定的类型实例当作同步变量使用。
先从最经典简易的同步类说起。
Lock() {
::InitializeCriticalSection(this);
}
~Lock() {
::DeleteCriticalSection(this);
}
void Enter() {
::EnterCriticalSection(this);
}
void Leave() {
::LeaveCriticalSection(this);
}
};
这是windows下实现线程同步最常见的封装。只需声明一个Lock实例,在需要同步的代码前后分别调用Enter和Leave即可。
既然用起来这么简单,为什么还要继续改进?显然这种方法有个很大的缺陷,如果忘了调用Leave,或者在调用之前就return/throw退出,那么就会引起死锁。
所以,我们需要类似auto_ptr的机制,自动维护栈数据的创建和删除。就暂且称它_auto_lock吧。
Lock& _lock;
_auto_lock(Lock& lock) : _lock(lock) {
_lock.Enter();
}
~_auto_lock() {
_lock.Leave();
}
};
_auto_lock通过引用一个Lock实例来初始化,并立即锁住临界区;被销毁时则释放锁。
有了这个机制,我们再也不用担心忘了调用.Leave()。只需提供一个Lock对象,就能在当前语块自动加锁解锁。再也不用担心死锁的问题了。
void Test()
{
// code1 ...
// syn code
{
_auto_lock x(mylock);
}
// code2 ...
}
进入syn code的"{"之后,_auto_lock被构造;无论用那种方式离开"}",析构函数都会被调用。
上述代码类似的在stl和boost里都是及其常见的。利用栈对象的构造/析构函数维护局部资源,算是C++很常用的一技巧。
我们的目标又近了一步。下面开始利用经典的宏定义,制造一颗synchronized语法糖,最终实现这样的语法:
void Test()
{
// code1 ...
synchronized(mylock)
{
// sync code
}
// code2 ...
}
显然需要一个叫synchronized宏,并且在里面定义_auto_lock。
乍一看这语法很像循环,并且要在循环内定义变量,所以用for(;;)的结构是再好不过了。
不过sync code我们只需执行一次,所以还需另一个变量来控制次数。由于for里面只能声明一种类型的变量,所以我们在外面再套一层循环:
synchronized宏将mylock替换成上述代码,既没有违反语法,也实现相同的流程。得益于循环语法,甚至可以在synchronized内使用break来跳出同步块!
我们将上述代码整理下,并做个简单的测试。
#include <windows.h>
#include <process.h>
struct Lock : CRITICAL_SECTION {
Lock() {
::InitializeCriticalSection(this);
}
~Lock() {
::DeleteCriticalSection(this);
}
void Enter() {
::EnterCriticalSection(this);
}
void Leave() {
::LeaveCriticalSection(this);
}
};
struct _auto_lock {
Lock& _lock;
_auto_lock(Lock& lock) : _lock(lock) {
_lock.Enter();
}
~_auto_lock() {
_lock.Leave();
}
};
#define synchronized(lock) for(int _i=0; _i<1; _i++)for(_auto_lock _x(lock); _i<1; _i++)
// ---------- demo ----------
Lock mylock;
// ---------- test1 ----------
void WaitTest(int id)
{
printf("No.%d waiting...\n", id);
synchronized(mylock)
{
Sleep(1000);
}
printf("No.%d done\n", id);
}
void Test1()
{
_beginthread((void
C++11 标准推出了一个新的关键词auto,这个关键词可以通过表达式自动推断返回值的类型,这也是新标准中被各编译器厂商支持最为广泛的特性之一。利用这个关键词可以有效减少代码的长度,特别是在使用模板元编程的时候。
举个简单的例子:
vector<map<int, string>> stringMapArray;
// 不使用auto版本
vector<map<int, string>>::iterator iter1 = stringMapArray.begin();
// 使用auto版本
auto iter2 = stringMapArray.begin();
看到这样简短的式子,我再也不能忍受第一个包含大量冗余信息的式子了。所以,最近写的C++里到处都是auto,恨不得在lambda的参数列表里也用(可惜不行)。
但是物极必反,auto的滥用却使一个非常隐蔽的问题悄然出现。最近写一个正则引擎的时候,发现运行效率总是低于预期,刚开始认为是动态内存分配的问题,通过替换成内存池后发现虽然效率有所提高,但是仍然达不到要求。于是想到了性能工具,一次检查下来发现如下语句竟然占用了70%的时间:
// 函数声明
set<int>& getSet();
void foo()
{
// 占用70%的时间
auto s = getSet();
...
}
检查发现原来auto把函数的返回值推断成了set<int>而不是set<int>&这使得在赋值的时候发生了集合的复制,而这个集合包含了6万多个数据,这使得本应几乎不花时间的引用操作变成了耗时巨大的复制操作。
查了下C++标准ISO/IEC 2011 P157,auto关键词的类型推断其实和模板的类型推断一致
§ 7.1.6.4 auto specifier
If the list of declarators contains more than one declarator, the type of each declared variable is determined as described above. If the type deduced for the template parameter U is not the same in each deduction, the program is ill-formed.
const auto &i = expr;
The type of i is the deduced type of the parameter u in the call f(expr) of the following invented function template:
template <class U> void f(const U& u);
所以之前的表达式相当于:
set<int>& getSet();
template <typename T>
void foo(T t);
void bar()
{
// 这里的参数T推断成了set<int>,而不是set<int>&
foo(getSet());
}
如果我们想要获得引用类型,就要显式地写:
auto& refValue = getSet();
经过修改之后,这条语句的执行时间已经下降到忽略不计了。
这个教训告诉我们,使用auto的时候虽然省事,但还是需要仔细考虑一下得到的类型到底是什么。
本文链接
虚函数与虚继承寻踪
封装、继承、多态是面向对象语言的三大特性,熟悉C++的人对此应该不会有太多异议。C语言提供的struct,顶多算得上对数据的简单封装,而C++的引入把struct“升级”为class,使得面向对象的概念更加强大。继承机制解决了对象复用的问题,然而多重继承又会产生成员冲突的问题,虚继承在我看来更像是一种“不得已”的解决方案。多态让对象具有了运行时特性,并且它是软件设计复用的本质,虚函数的出现为多态性质提供了实现手段。
如果说C语言的struct相当于对数据成员简单的排列(可能有对齐问题),那么C++的class让对象的数据的封装变得更加复杂。所有的这些问题来源于C++的一个关键字——virtual!virtual在C++中最大的功能就是声明虚函数和虚基类,有了这种机制,C++对象的机制究竟发生了怎样的变化,让我们一起探寻之。
为了查看对象的结构模型,我们需要在编译器配置时做一些初始化。在VS2010中,在项目——属性——配置属性——C/C++——命令行——其他选项中添加选项“/d1reportAllClassLayout”。再次编译时候,编译器会输出所有定义类的对象模型。由于输出的信息过多,我们可以使用“Ctrl+F”查找命令,找到对象模型的输出。
一、基本对象模型
首先,我们定义一个简单的类,它含有一个数据成员和一个虚函数。
{
int var;
public:
virtual void fun()
{}
};
编译输出的MyClass对象结构如下:
1> +---
1> 0 | {vfptr}
1> 4 | var
1> +---
1>
1> MyClass::$vftable@:
1> | &MyClass_meta
1> | 0
1> 0 | &MyClass::fun
1>
1> MyClass::fun this adjustor: 0
从这段信息中我们看出,MyClass对象大小是8个字节。前四个字节存储的是虚函数表的指针vfptr,后四个字节存储对象成员var的值。虚函数表的大小为4字节,就一条函数地址,即虚函数fun的地址,它在虚函数表vftable的偏移是0。因此,