分类目录归档:社区精华

从抽象谈面向对象与泛型编程 part.1

1. 前言

抽象是我们常用的思维过程。一系列事物通过大脑的提炼,归纳和综合,让我们可以从无序中找出有序,从一个个具体的问题中找出通用的解决方法。编程中的抽象是从面相对象的程序设计,后文称为OOP,开始才有了比较完善的语言支持。如今软件复杂程度越来越高,OOP一直都是解决复杂软件设计的重要方法。使用OOP可以屏蔽各个任务具体的细节,抽象出一个简化而统一的流程,从而降低复杂软件开发的难度。这是一个建模的过程,也是OOP能够成功解决问题的原因。但是OOP的抽象能力是有限的,OOP的滥用反而会使复杂度无控制地上升。鄙文会浅讨OOP抽象适用的场景,并会阐述为什么作者觉得everyting is object是一个错误的设计。这个系列的文章后面还会探讨另外一种抽象能力更强大的范式,泛型编程GP。鄙文不是引战OOP与GP孰优孰劣,而是以抽象为线索,浅讨如何编写出能应付更加复杂程序的方法。

2. 传统OOP中的抽象

OOP有一个经典的案例。鱼会游泳,鸟会飞行,而它们都是动物。于是用OOP就很自然的有了下面的代码:

许多文章和书籍对OOP都是这样教的。从animal继承以扩展新的动物类,如哺乳类。从bird或fish继承,以扩展出具体的物种。当然还有许多文章也拿这个例子作为OOP的反面教材。因为当我们要实现飞鱼的时候,飞鱼既可以游泳,也可以飞行,这个继承结构无所适从。那么究竟是哪里出了问题?

熟悉OOP的人会说,这里违反了里氏里氏替换原则(LSP)。飞鱼属于鱼类,但是我们抽象的鱼类无法满足要求。那我们给鱼增加一个fly接口:

这能解决飞鱼这一个情况,但现实中好像还有更多复杂的情况。例如天鹅属于鸟类,但是它既会走路,也会飞行,当然也会游泳。这个时候我们又不得不扩充鸟类的接口。问题似乎变复杂了,绝大部分的鱼并不会飞行,但是为了兼容飞鱼我们为鱼类添加了fly接口。其实,真正的问题在于,我们为了实现一个可以游泳的动物,根据有限的经验抽象出了鱼类,并把这个动物归为鱼类。这种抽象的方式的代价,就是给实现类与抽象类之间,增加了一个is_a的关系,这个关系是一个很强的依赖关系。

对于我们举的这个例子而言,这种错误很容易发现,这是得益于动物这个话题是人们所熟知的。但是现实的问题往往都是未充分了解的,所以我们的抽象会很容易地违反LSP。其次,仅仅使用接口的异同类对事物进行抽象和归属,似乎并不是一个很好的主意。于是OOP有了改良,对行为的抽象看起来要更合理一些。

3. 接口编程与行为抽象

来到了接口编程,我们不再为类别进行抽象了,而是抽象行为。例如,上面的例子,我们不再为动物区分哺乳类,鸟类和鱼类等,也不会为鱼类实现飞鱼或者鲈鱼等其他具体的物种。简单起见,假设动物的行为有飞行,行走和游泳,于是我们抽象出了这三个接口。

飞鱼既可以飞行也可以游泳,我们只要实现这两个行为,飞鱼的问题就很好地解决了:

如果语言不支持多重继承,可以使用组合的方式实现。目前的设计好像能应付所有的动物,甚至能推广得更远。例如,实现飞机也很容易:

现在我们要实现一个功能,让我们实现的飞鱼先飞行再游泳。在OOP中,对象的具体语义是不可知的,例如我们访问飞鱼的fly接口是通过flyable接口对象,对于swim同样是从swimmable对象访问,而我们并不知道flyable和swimmable是否是从飞鱼而来的。OOP在这里会带来运行期的额外开销。呃,这里好像碰到了点什么问题。我们如何保证flyable和swimmable两个接口对象,都来自于同一个飞鱼对象?

在我们继续往下解决这个问题之前,让我们在回想一下,为什么要抽象。抽象是为了屏蔽每个任务具体的细节,提炼出一个统一的处理流程。也就是说,统一的处理流程是我们抽象的目标。而现在的这个先飞行再游泳的功能,无法在我们现有抽象的模型中表达。问题已经很清楚了,是我们的抽象出了问题。这就是OOP在设计的时候一定要避免的误区,我们的抽象是为了得到统一的处理流程,而不是为了适应更广泛复杂的真实世界的模型而过度抽象,遵守KISS很重要。

对于这个例子,如果我们的需求只是希望在程序中,统一控制实体先飞行再游泳,应该这样抽象

如果我们的需求是想统一控制实体的fly和swin行为,并能够灵活地组合这些行为,可以这样抽象:

OOP的抽象的程度应该到此为止,如果问题更复杂可以适当使用设计模式解决,当然设计模式不是鄙文探讨的范围。坚守KISS原则是一件很难的事情。Everything is object,这个极具诱惑和理想主义的咒语,一直在不停地将我们拉入深渊。是的,一直有一条路通往那里。

4. 深渊,从这里开始

下面的设计,不应该归于OOP的范畴了。因为它们为了试图用统一的方法解决一切问题,而且都违背了KISS最基本的教义。首先,如果我们想要继续坚持之前的设计,又希望问题能够得到解决,本着接口统一的原则,我们可以像COM一样,提供一个接口查询的机制:

还有一个解决方案,就是使用发送消息:

以上两个方法很类似,就放在一起讨论。两个方法都引入了一个共同的公共基类,object_base. 对!Everything is object就从这里开始了。第一种方法提供了一个统一的接口查询机制,而后者提供了一个统一的发送消息机制。两者都需要全局唯一的ID来区分不同的接口或者消息。接口查询的方式会带来接口对象不安全强转,而发送消息会招致不安全的数据转换。就目前而言所引入的公共基类和全局ID的负载,还有不安全的代码都不是很严重的问题。例如后者,可以在对象语义完整的时候,创建代理来绑定操作,从而抵消不安全的代码。最严重的问题是,OOP在这里已经荡然无存了。没有了接口的约束,之前抽象的模型也毫无意义了,这于过程式编程毫无差别。如此抽象的结果,却是写出了等同之前毫无抽象的代码,这种设计难道不是一个悖论吗?

深渊也是有底部的,我们继续把查询接口与查询消息推广,其实就是现代语言都提供的反射。反射是一种内省机制,可以让我们查询对象的成员方法和成员数据。当然,与其这样使用反射,何不去使用一门动态语言?

5. 小结

OOP使用的接口抽象的方法,本质上是围绕接口进行的归类方法。这种方法可以做到统一调度流程,屏蔽具体的细节,但同时也许多代价:
1. 围绕接口进行抽象,只利用了语法约束,但丢弃了语义;
2. 归类引入了很强的is_a的依赖关系,而程序中的归类结果通常与现实中经验相差甚远;
3. 对于静态类型,有很明确的编译期的语言,OOP把负担都扔给了运行期;

OOP有着适中的抽象能力,用来对现实问题的简化模型进行抽象,而everything is object是OOP的滥用。

下一篇,我们将讨论静态强类型语言所擅长的泛型编程。GP具有比OOP强大许多的抽象能力,它比OOP更适合于抽象复杂的模型。我们会以最大公约数算法为线索,讨论GP在C++编程语言中的实践方法。

解耦利器function message bus

需求1

能把模版函数和一个key注册起来以便后面使用,能把参数不同的函数和一个key注册起来以便后面使用。
c++中没有这样的一个容器可以存放模版函数和参数类型不同的函数。

需求2

对象A和对象B相互调用,耦合性很强,如何消除这种耦合性;
对象A和对象B没有任何关系,但A希望用B里面的方法,但二者又不适合直接关联起来。

如果你碰到这两个需求中的任何一个,那么你就需要function message bus。

作用

1.作为一个万能的函数注册器,可以注册任意类型的函数(除了重载函数);

2.解耦对象之间的调用关系

例子

上面的例子展示了如何注册不同类型的函数,包括普通(模版)函数,lambda和成员函数,也支持函数返回值。

因为函数的调用者不必知道被调用者,二者都依赖于function_msg_bus,所以你就可以通过它来解耦对象之间的调用关系。

具体代码

github

再谈自动注册的工厂

关于自动注册的工厂,我之前写过了两篇文章,之前的一篇文章在这里。这个自动注册的工厂通过宏实现自动注册,同时也可以支持变参。

下面是通过宏实现自动注册的例子。

之前提到的自动注册的工厂

问题

通过宏实现的自动注册工厂的主要问题就是需要借助宏,宏有两个问题,第一个问题是不能调试,第二个问题是让代码变得晦涩。这些自动注册的宏散落在各处,也增加了维护负担。如果能不借助宏实现自动注册就可以消除这些问题了。

改进目标

之前的自动注册工厂还需要改进,改进的目标就是消除宏。

实现

实现代码很简单,主要思路是借助crtp和静态变量初始化顺序来实现的,因为静态变量的实例化是早于main函数的,因此我们可以利用静态变量实例化的时候实现自动注册。

创建的对象需要派生于AutoMsgFactory,并实现一个静态的id函数,这个id就是创建该类型对象需要的一个唯一的id。需要注意的一个细节是:在id函数中需要使用一下基类AutoMsgFactory中的静态变量registered_,用来保证静态变量实例化。

assert(registered_);

上面这行代码在这里有两个作用,第一个作用是调用了registered_保证当前类会被自动注册,第二个作用是避免重复注册,出现重复注册时会触发断言错误。

另外通过编译期限定派生类必须定义一个Id函数可以保证派生类不会忘记定义创建该类需要的唯一id,这个id是自定义的,可以是枚举类型也可以是整形。

总结

这个实现消除了宏,在使用上也比较方便,不过也有个缺点是需要调用一下静态变量。综合来看,个人感觉比通过宏实现自动注册更好。

更新 支持变参

Feather–一个快速开发的web服务器框架

Feather是一个适合快速开发的modern c++ web框架,Feather的目标是让使用者以最小的精力和成本来开发一个web网站。

现在很多web框架都非常庞大,学习成本高,而Feather正是为了解决这些问题的,它就像它的名字feather那样轻盈,所有的一切都是为了让用户非常方便又快速地开发而不是陷入到框架的细节当中。

一个好的框架应该是能让用户轻松实现自己的想法而不是成为框架的奴隶。如果你希望轻松快速地开发一个web网站,而无需花费大量的精力去学习框架细节的话,那么Feather非常适合你!

Feather是什么?

Feather作为一个快速开发框架,它的目标是让web开发变得简单。它主要有下面几个特点:

  1. 简洁易用
  2. 高性能,modern c++(c++17)开发
  3. header only
  4. 跨平台
  5. 支持编译期反射
  6. 支持AOP

Feather框架的核心包括:
1. 一个易用的http库,支持http1.x, https, websocket
2. 一个功能强大的html模版引擎
3. 一个可扩展的ORM库,支持多种数据库(mysql,postgresql,sqlite)
4. 一个可扩展序列化库,支持多种格式(json, xml)

Feather的架构

下面是Feather的架构图:

Feather的架构图

Feather内部的四个核心组件是松耦合的,Feather只是把它们像搭积木一样搭在一起。

  1. http组件: cinatra
  2. ORM组件: ormpp
  3. 序列化组件: iguana
  4. html 模版: inja

Feather的使用

以一个简单的例子展示如何使用Feather,比如显示文章列表,几乎每个网站都有这个功能。

获取文章列表的功能很简单,它底层数据部分是一个文章表,业务逻辑层是获取这些列表,然后做html渲染。对外接口部分是一个http服务,前端后端交互的数据格式为json。为了简单起见就不考虑缓存之类的。

那么要实现这个文章列表功能用Feather怎么做呢?可以按照这些步骤去开发:

  1. 提供获取文章列表的http接口;
  2. 通过ORM提供数据库访问功能;
  3. 编写业务逻辑,根据用户请求获取文章列表并通过html模版做渲染;

接下来看看这些步骤具体是怎么做的。

获取文章列表的http接口

其中login接口是这样的:

接下来就可以测试这个http服务接口了,客户端只要发送一个http请求即可。比如发送一个这样的请求

http://127.0.0.1/get_article_list

服务器会自动路由到article_controller::get_article_list函数中,如果请求不对则会返回http错误给客户端。当服务器收到这样的请求之后就表明服务器提供的这个http服务是可用的。

接下来需要编写数据库部分的代码,由于有了ORM,所以你可以很方便地编写数据库部分的代码了,同样很简单。

通过ORM提供数据库访问功能

登录业务涉及到一个用户表,因此我们需要创建这个表,不过在创建数据库之前先确定你选用什么数据库,Feather的ORM支持mysql, postgresql和sqlite三种数据库,假设我们的数据库是mysql。我们可以通过下面的代码来创建一个用户表。

1.创建文章表

dao.create_table< article >将会在testdb数据库中自动创建一个article表,其中id字段是自增长的。

2.编写获取文章列表的逻辑(包含访问数据库)

访问数据库,序列化为json返回给客户端。

详细的例子你可以看github上的代码

Demo示例

我们用Feather开发了一个社区网站,地址在这里:http://120.78.135.113:8080/

Feather社区

致谢

Feather社区网站由我和网友XMH共同开发完成(花了两天的业余时间),XMH也是cinatra的忠实用户,不但贡献了很多代码,还提供了很多宝贵的建议,在此表示衷心的感谢!

XMH是一名热爱编程的程序猿,平时从事c++,web,移动端等开发。从事过游戏后台和APP开发。也是个忠实的mordern c++粉丝,追随着cpp的发展,喜欢通过元编程做一些小工具。热爱开源社区,也是开源项目feather的使用者,希望feather能为更多的开发者所使用,社区发展越来越好。

希望有更多人能加入进来把Feather完善得更好。

联系我们

purecpp@163.com

http://purecpp.org/

https://github.com/qicosmos/feather

推荐下本人的无栈协程库librf

https://github.com/tearshark/librf

librf

librf – 协程库

librf是一个基于C++ Coroutines提案 ‘Stackless Resumable Functions’编写的非对称stackless协程库。

目前仅支持:
Windows (使用2017编译)(由于使用了SFINAE导致不再支持VS2015)

librf有以下特点:

  • 1.基于C++17提案’Stackless Resumable Functions’编写的非对称stackless协程库,可以以同步的方式编写简单的代码,同时获得异步的性能
  • 2.理论上支持海量协程, 创建100万个协程只需使用物理内存
  • 3.提供协程锁(mutex), 定时器, channel等特性, 帮助用户更加容易地编写程序
  • 4.可以很好的跟asio,libuv等库结合,能跟现有的callback范式的异步/延迟代码很好的结合
  • 5.目前还处于实验状态,不对今后正式的C++ Coroutines支持有任何正式的承诺

  • 如果你发现了任何bug、有好的建议、或使用上有不明之处,可以提交到issue,也可以直接联系作者:
    email: tearshark@163.net QQ交流群: 296561497

  • doc目录下有作者搜集的一些关于C++协程的资料

  • tutorial目录下有针对每个特性的范例代码,让用户可以循序渐进的了解librf库的特性

一个更好的自动注册工厂

在几年前我介绍过一种C++11实现的自动注册工厂,这是工厂模式的一种优雅的实现。在这里我们需要明确一个概念就是工厂模式,它是如何优雅地解决一个产品族的创建问题。所谓产品族就是一个继承体系的产品,比如有一个产品Message,它是一个基类,有很多Message是从它派生而来的,比如有Message1,Message2,Message3…等很多产品。

这些产品的创建依赖于某个key,类似于这样:

这是一个典型的工厂方法,这种写法在产品不多的时候是没问题的,但是如果产品越来越多的时候,switch-case就会越来越长,导致难以维护。另外还存在一个问题,有的产品不是无参数的构造函数,如果有些产品依赖了不同的参数,那么这个工厂方法是无法满足需求的。

之前介绍的自动注册工厂解决了switch-case膨胀的问题,但是对于需要参数的产品的创建没有解决得很好,需要进一步改进。改进的办法是把参数作为factory的模板参数,这样就可以解决有参数需求的问题了。

下面是具体实现:

这是一个Messgae产品族的工厂类,有了这个类之后我们就可以很方便地创建各种产品了。下面是测试例子:

Message产品族有4类产品,有的是无参的,有的是多个参数的产品,现在都可以统一创建了,直接输入key和构造参数即可,这个key可以自行修改为int或者枚举变量。

需要注意的是msg4,因为它注册的时候提供了一个function,让function提供创建功能,以满足更灵活地需求。

有了这样一个工厂类之后我们就可以很好地解决产品族创建的问题了。你还可以基于此把它改成一个抽象工厂类,但我觉得你应该慎重考虑一下是否有必要,一般情况下工厂模式就够了,不需要引入更多的复杂性。

tensorflow variant源码分析

tensorflow variant基本语义

通过分析tf.variant的源码可以知道它其实是一个any语义,即这个类型可以被任意类型赋值,它的主要目的就是做彻底的类型擦除。

这个名字取得有迷惑性,它和标准库和boost库中的variant语义是不一样的,而是和c++17中的std::any对应的。

tf.variant用法

可以从tf的测试代码中知道它的基本用法,用法很简单:

从这个测试代码中可以看到tf.variant和std::any/boost.any用法是差不多的。

tf.variant内部有一个unique_ptr指针, 默认为nullptr,所以没有赋值的时候总是返回nullptr。

Int(42)赋值给variant之后,就可以通过get(x)来获得Int指针了,接着就可以得到其实际的Int值了;因为这个Variant是any语义,所以任意类型比如Tensor也可以复制给它,取值方法也是类似的,先取T指针,接着调用value得到初始值。

注意,这里的get中类型必须和赋值时候的类型一致,不一致的时候会返回nullptr(除了void,传void时会返回void*指针);

any和variant比较

之前介绍过vairant,知道它也是用来做类型擦除的,不过variant擦除的类型是有限个的,必须要事先指定,它只能实现部分的类型擦除。

而any也是实现类型擦除的,它可以代表任何类型,不需要像variant那样需要事先确定擦除的类型,看起来更方便更强大了。

事实上这个any并不是变得更强大了,它虽然能彻底擦除类型,但是,在取值的时候需要知道准确的类型,这反倒不如variant方便了,variant在取值的时候通过一个visitor是不需要知道具体类型的。

二者的本质区别是类型擦除方式不一样,variant是通过栈上的一个定长内存块去保存赋值对象的,variant中所有的类型共用这块buffer,而any是通过派生,在堆内存上创建一个实际的对象,并且每次赋值都会析构之前的对象,重新在堆内存上重新创建一个新对象。

所以从性能上说variant的效率是高于any的,二者效率大约相差4倍左右。 附一个测试代码:

所以能用variant就不用any,除非真的需要擦除所有的类型,或者是为了追求代码更简单不太在意性能的时候可以用any,大部分情况下用variant擦除部分类型就可以满足需求了。

any的一个典型应用场景就是http服务器中的session,这个session是保存用户在服务器端的数据的,这个数据类型可能是字符串,数值,或者用户自定义的对象,在这种情况下无法定义一个variant,这时候用any是合适的。

tf.variant的实现

tf.variant的实现思路

tf.variant的实现思路实际上就是any的实现思路,any对象内部有一个内部基类的unique_ptr,这个基类有一个带模版参数的派生类。

这个派生模版类是泛型的,用它代表所有类型。

当给any对象赋值的时候,我们就创建这个派生类对象,这个对象保存了实际的类型。

在后面取值的时候我们就可以将这个基类向下转型为某个具体的派生类对象了,这时需要判断当前保存的对象类型和传入的类型是否一致,不一致则返回nullptr。

any

tf.variant的源码实现

1.内部定义一个抽象类和一个派生模版类

2.定义一个成员变量,内部接口类的指针–std::unique_ptr< ValueInterface > value_;

3.赋值时创建派生类对象

赋值的时候对赋值类型做了限定,必须为非Variant的,并且有拷贝构造函的类型,因为any内部会重新创建这个对象,之后赋值给基类指针value_,同时实现了类型擦除。

4.取值

取值的时候会判断是否初始化以及类型是否匹配,满足条件就转成实际的类型,否则返回nullptr。

这个代码的实现还是比较简单的,就是实现了一个基本的any语义,还有一些自己扩展的部分,比如encode和decode之类的。

总结

tf.variant本质上就是一个any,用来做类型擦除,像c#,java中的object那样代表一个通用类型。

在tensorflow中很多时候是作为一个函数参数,有些这样代码:

目前还没进一步分析tf的代码,我猜测这样做的目的可能是为了统一的接口以及灵活性,接口可以保持通用不变,具体实现将因具体类型不同而各有不同,方便扩展。

variant原理和应用

variant原理和应用

variant语义

variant是一个泛化的、类型安全的union。可以保存类型不同的对象,它的特点是多类型单值。

基本用法

以c++17中的variant为例(boost中的variant和标准库的用法几乎一样),我们定义一个这种的variant:

std::variant<int, double, char> v;

这个variant可以用来存储int, double和char三种类型的数据,所以我们可以这样赋值:

可以看到类型的值可以赋给同一个variant,如果将一个非int, double和char的类型赋值给v的话则在会出现一个编译期错误,所以variant是类型安全的,variant只允许将定义时指定的那些类型赋值给它。注意,重新赋值的时候之前的对象会自动析构掉。

接下来看如何取值:

通过std::get(v)就可以获取对应的值了,不过取值的时候需要知道当前的variant具体是什么类型,如果类型不对则会抛异常。

这种需要传具体类型的访问variant的方法有局限性,有时候我们不能确切知道当前的具体类型,这种情况下该如何取值呢?
以boost库提供的访问vairant方法为例:

这种访问方法需要定义一个函数对象对每个类型的访问都写一个重载函数,如果找不到对应的重载函数则会报一个编译期错误。

这种方法更常用,因为它不需要知道具体的类型。

还可以用C++11写一个支持lambda的visitor,避免写一个函数对象,用起来可以更简单。

实现原理

虽然很多库自己实现了variant,但其实现原理是类似的,实现variant主要分为下面几步:

1.variant内部定义一个足够大的缓冲区。

足够大的意思是这个缓冲区可以用来创建variant定义时的那些类型,具体多大呢,需要借助模版元方法遍历所有的类型找出其中size最大的那个类型,找到这个最大的size之后我们就可以定义一个栈上的char数组了,这里还需要考虑内存对齐的问题。

2.通过placement new创建某一个类型的对象。

这个对象就在内部的栈上缓冲区中创建的,所有的类型共用这块缓冲区,和union类似。具体创建对象的时候用的是placement new,一个是出于性能考虑,一个出于便于回收之前的对象考虑,因为重新赋值的时候需要将之前的对象析构掉。

3.variant赋值。

赋值时需要保存当前类型的type_index, 便于后面取值的时候判断需要取值的类型和当前类型是否一致。

4.variant的析构。

析构的时候要根据type_index遍历所有的类型找到当前的类型然后调用该类型的析构函数去析构。

通过这个思路就可以实现一个基本的variant,如果需要支持更多功能的时候还需要增加一些代码,比如支持嵌套的variant之类,支持lambda访问等等。

具体的实现可以参考我的github上C++11实现的基本功能的variant:https://github.com/qicosmos/cosmos/blob/master/Variant.hpp.

应用场景

我们一般在什么场景下需要用到variant呢?varaint一般用于类型擦除,比如我们需要访问一个某个值,这个值的类型是有限个的,这时可以用variant来表示这个“可能是多个类型的值”,避免针对具体类型去写代码,可以消除强制转换,或者把复杂问题简化,比如一些异构类型没办法抽象泛化的时候,用variant泛化是很方便的。

一个典型的应用场景就是数据库表访问的场景,数据库表字段的类型是有限多个的,我们完全可以用一个variant来表示数据库字段,接着就可以像通用类型那样去访问数据表字段了,而不必关注具体的类型,把一个复杂问题变得很简单了。

如何引入到我们的工程

boost库中已经有variant了,c++17中也引入了,如果我们的编译器还只支持C++11,则可以直接用boost中的variant,boost中的variant是header only的,直接包含头文件既可以,这里涉及到另外一个问题就是如何引入boost库。

boost库是一个广泛使用具有工业强度的c++库,很多C++新标准里的新特性都是来自于boost,使用里面已经有的库,可以避免重复造轮子,节省开发测试时间,而且质量是有保证的。

那么如何引入boost呢,引入boost有两种方式:

1.直接安装

可以查看当前源中有哪个boost库, 然后直接安装:

安装之后直接在工程中引用boost头文件既可以,以variant为例,使用variant时直接include即可:

2.编译安装

从boost.org官网下载一个版本,解压之后在boost目录执行两个命令就可以了

总结

variant作为一个类型安全的union,可以帮助我们做类型擦除,从而可以避免强制转换,便于编写更加泛化通用的代码,化繁为简。

cinatra–一个高效易用的c++ http框架

目录

cinatra简介

cinatra是一个高性能易用的http框架,它是用modern c++(c++17)开发的,它的目标是提供一个快速开发的c++ http框架。它的主要特点如下:

  1. 统一而简单的接口
  2. header-only
  3. 跨平台
  4. 高效
  5. 支持面向切面编程

cinatra目前支持了http1.1/1.0和websocket, 你可以用它轻易地开发一个http服务器,比如常见的数据库访问服务器、文件上传下载服务器、实时消息推送服务器,你也可以基于cinatra开发一个mqtt服务器。

如何使用

编译依赖

cinatra是基于boost.asio开发的,所以需要boost库,同时也需要支持c++17的编译器,依赖项:

  1. boost.asio, boost1.66
  2. c++17编译器(gcc7.2,clang4.0, vs2017 update15.5)

使用

cinatra是header-only的,直接引用头文件既可。

快速示例

示例1:一个简单的hello world

5行代码就可以实现一个简单http服务器了,用户不需要关注多少细节,直接写业务逻辑就行了。

示例2:展示如何取header和query以及错误返回

示例3:面向切面的http服务器

本例中有两个切面,一个校验http请求的切面,一个是日志切面,这个切面用户可以根据需求任意增加。本例会先检查http请求的合法性,如果不合法就会返回bad request,合法就会进入下一个切面,即日志切面,日志切面会打印出一个before表示进入业务逻辑之前的处理,业务逻辑完成之后会打印after表示业务逻辑结束之后的处理。

示例4:文件上传

cinatra目前支持了multipart和octet-stream格式的上传。

multipart文件上传

短短几行代码就可以实现一个http文件上传的服务器了,包含了异常处理和错误处理。

octet-stream文件上传

示例5:文件下载

示例6:websocket

性能测试

测试用例:

ab测试:ab -c100 -n5000 127.0.0.1:8080/

服务器返回一个hello。

在一个8核心16G的云主机上测试,qps在9000-13000之间。

对比测试

通过ab测试和boost.beast做对比,二者qps相当,大概是因为二者都是基于boost.asio开发的的原因。cinatra目前还没做专门的性能优化,还有提升空间。

注意事项

文件上传下载,websocket的业务函数是会多次进入的,因此写业务逻辑的时候需要注意,推荐按照示例中的方式去做。

cinatra目前刚开始在生产环境中使用, 还处于完善阶段,可能还有一些bug,因此不建议现阶段直接用于生产环境,建议先在测试环境下试用。

试用没问题了再在生产环境中使用,试用过程中发现了问题请及时提issue反馈或者邮件联系我。

测试和使用稳定之后cinatra会发布正式版。

roadmap

  1. 支持ssl
  2. 支持断点续传
  3. 支持session和cookie
  4. 接口优化、性能优化

我希望有越来越多的人使用并喜欢cinatra,也希望cinatra在使用过程中越来越完善,变成一个强大易用、快速开发的http框架,欢迎大家积极参与cinatra项目,可以提issue也可以发邮件提建议,也可以提pr,形式不限。

这次重构的cinatra几乎是重写了一遍,代码比之前的少了30%以上,接口统一了,http和业务分离,具备更好的扩展性和可维护性。

联系方式

purecpp@163.com

http://purecpp.org/

https://github.com/qicosmos/cinatra

致谢

感谢社区的“逐雁南飛”和“非常可乐”两位朋友的帮助,你们帮我澄清了一些http业务细节,同时也提出了一些宝贵意见,在此致以衷心的感谢!

一个C++14模板元实现的深度学习神经网络模板类,支持任意层数

构造编译期矩阵以及数据传递代码,headonly
搜遍了github,在模板元这块机器学习还是空白,正好是个填补,我接下来会逐渐丰富这个库(倒是有几个模板元数学运算库,都很简陋)
大量的矩阵运算用模板元进行有几个让人非常惬意的优势,也发觉模板元其实很适合这种编程
(不知道是否唯有C++才有的优势,数学专用语言不算在内,比如m、r这些):
1、永远不用担心数组越界,也不用写检查数组越界的代码
2、矩阵运算不用检查行列是否匹配,行列的要求通过模板函数参数就能限定了
3、快,只有cpper才懂的快
代码在这里 https://github.com/bowdar/DeepLearning

先看使用方法,过程极其简单

模板类的申明,开头是用来迭代整形模板参数的UnpackInts,根据Index取值,没有使用TypeList
代码使用到的矩阵模板类和数学公式就没贴了

模板类的实现