我对C++的使用和理解是不断变化的。从一开始的C with Class 到接触到设计模式,才理解了什么叫真正的OOP;从接触到STL才真正正视Template,了解了什么是GP;从Python和Golang的火热中了解了函数作为first-class的力量;从某些文章对Lisp近乎玄学的推崇中知道了FP的优势和它逐渐在主流的编程方式中兴起的原因。
C++是包容和自由的,自从学了std::function和lambda之后,我也开始逐渐学着向FP方式转变。因为在很多方面,将function作为first-class,对编码带来的不只是形式上的变化,更是思维方式的变化。
lambda与QObject::Connect
C++11lambda表达式和Qt5的搭档,使得可以可以放弃SIGNAL
、SLOT
宏,采用一种更加直观和简洁的方式使用Connect。
使用lambda之前,如果要在Qt的main函数里使用slot,不得不另外构造一个类继承QObject,并且定义槽函数,然后才能在main里实例化对象并绑定槽函数:
//myObject.h
#include <QObject>
class myObject : public QObject {
public:
myObject(QObject* parent = 0);
~myObject();
public slots:
void onClicked();
};
//myObject.cpp
......
void myObject::onClicked(){
qDebug() << "clicked";
}
//main.cpp
#include <QApplication>
#include <QDebug>
#include <QPushButton>
#include "myObject.h"
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
QPushButton *button = new QPushButton("click");
button->show();
myObject *obj = new myObject();
QObject::connect(button, SIGNAL(clicked()), obj, SLOT(onClicked()));
return app.exec();
}
为了一个槽函数,还要引入另一个类,实在是大动干戈,而且还不直观(这可能也是OOP为人诟病的一个方面吧)。在有了lambda之后,是这么干的:
#include <QDebug>
#include <QPushButton>
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
QPushButton *button = new QPushButton("click");
button->show();
QObject::connect(button, &QPushButton::clicked, []() {
qDebug() << "clicked";
});
return app.exec();
}
简洁!一目了然!!
虚函数与std::function
上面的例子其实还有类似的情况,在QWidget上想要自定义鼠标press事件,我们有两种方式:
- 该widget外部使用eventfilter来拦截该widget的鼠标press事件并处理
- 自定义继承自QWidget的widget类并覆盖其
mousePressEvent()
的虚函数
第一种方式不直观,对该控件的处理要到别的地方去寻找,不是很“OOP”;第二种方式和上一例子一样,代价有点大。
其实这样的例子还有很多。传统的OOP在解耦的同时一定会导致体型的臃肿,除此之外还经常会有将处理流程隐藏在层层的封装和继承之中导致的不直观不清晰的问题。
代码说到底是人来写人来读的,任何反直观的都是不好的。编程就像写文章,诘屈聱牙的东西没人愿意看,好的代码一定是读起来酣畅淋漓的。
如果我们自己实现一个Button类,可以是这样的:
//Button.h
class Button {
......
virtual void onClicked() = 0;
};
//MyButton.h
class MyButton : public Button {
......
void onClicked(){
//需要的操作
}
};
这样,在我们需要一个Button的时候,新写一个类继承Button,将点击的处理写在onClicked
方法内即可。可以,这很“OOP”。
现在呢,我们可以利用std::function,使得函数作为类成员,像对待类的普通成员一样对函数成员进行赋值操作,即可得到我们需要的对象:
//Button.h
class Button {
......
std::function<void()> _onClicked;
};
//在使用的地方
Button btn;
btn._onClicked = [](){
//需要的操作
};
喏,更加的简单明了。函数不需要通过继承来特化,而是通过像普通变量一样的方式直接实例化,带来的不光是结构 上的简单,还有语意上的直白。
ScopeGuard
资源的释放从来都是一个问题。文件句柄、锁、等等资源,申请的时候我们可能想着一会儿用完要释放,等到用完之后可能就忘了,或者是因为分支处理漏掉了,这都是很有可能发生的,就算没有在分支处理中漏掉,在各个分支里都重复的写同样的释放资源的代码也很不fashion,没人愿意当CV战士。
利用RAII特性,局部变量析构时候释放资源已经成为一个通用做法。麻烦的是我们需要为各种资源都创建类来利用其析构函数释放资源,太麻烦。现在有了std::function就好了,借鉴一下std::lock_guard,就有了下面的做法:
//ScopeGuard.h
class ScopeGuard {
public:
explicit ScopeGuard(std::function<void()> callback)
: _onExit(callback) {};
~ScopeGuard(){
_onExit();
};
private:
std::function<void()> _onExit;
};
#define ON_SCOPE_EXIT(callback) ScopeGuard EXIT##__LINE__(callback)
//在使用的地方
{
HANDLE f = fopen("conf.yaml");
ON_SCOPE_EXIT([=](){
fclose(f);
});
......
{
_mutex.lock();
ON_SCOPE_EXIT([&](){
_mutex.unlock();
});
......
}
......
}
资源创建之后,立即跟在后面写释放方式,不会忘不会漏,看起来还明确。
ON_SCOPE_EXIT宏作用在于创建了一个ScopeGuard局部变量;变量名由行号确定,避免了多个ScopeGuard重名的问题。
其它
将function作为first-class带来的改变还有很多,比如将function保存在容器中,将function作为值传递给别的线程执行,返回闭包来创建累加器等等。
初极狭,才通人。复行数十步,豁然开朗。
网友评论