C++11之美_c++ mes
myzbx 2025-09-04 13:54 7 浏览
本文内容来源于我在SDCC2015上的演讲,演讲的主题是关于C++11/14的新特性,通过一些案例和旧标准C++98/03做了对比,展示了新特性在解决问题时的优雅与强大。
C++11/14相比以往的C++98/03在很多方面做了简化和增强,尤其是在泛型编程方面,让C++的泛型编程的威力变得更加强大,本文将用三个案例来介绍C++11相比C++98/03如何以一种更灵活、更简洁和更泛化的方式去解决问题。
检查成员函数是否存在
有这样一个场景,需要判断某个类型是否存在成员函数foo,如果存在就调用这个成员函数,否则就执行默认的函数,下面是伪代码。
template<typename T> void foo(const T& t) { if (t has foo) t.foo; else default; }
在有反射的语言里,如c#和java里,这是很容易的,然而C++没有反射,C++98/03中要实现这个逻辑,需要借助SFINAE(substitution is not an error)特性。实现这个逻辑分为两步,第一步在编译期检查成员函数foo是否存在,第二步根据第一步的检查结果来选择分支。C++98/03里的做法是这样的。
template<typename T> struct has_member_foo { template<typename U, void(U::*)> struct SFINAE {}; template<typename U> static char check(SFINAE<U, &U::foo>*); template<typename U> static int check(...); static const bool value = sizeof(check<T>(0)) == sizeof(char); };
元函数has_member_foo中定义了一个辅助结构体SFINAE和重载函数check,辅助结构体在模板实例化的时候可以确定类型U是否存在fooh函数;check重载函数利用了SFINAE(substitution is not an error)特性,编译器会根据重载决议选择最合适的函数,当辅助结构体实例化成功时,第一个check函数的优先级更高,如果实例化失败则会选择优先级最低的check(...)函数,这两个重载函数的返回类型不相同,我们根据函数返回类型的size就可知道类里是否存在成员函数foo了。下面是测试代码。
struct MyStruct { void foo { } }; static_assert(has_member_foo<MyStruct>::value, "true"); //断言通过 static_assert(has_member_foo<int>::value, "false"); //断言错误
C++11的实现方式是这样的。
template<typename T> struct has_member_foo11 { private: template<typename U> static auto check(int) -> decltype(std::declval<U>.foo, std::true_type); template<typename U> static std::false_type check(...); public: enum { value = std::is_same<decltype(check<T>(0)), std::true_type>::value }; };
C++11的实现不需要辅助结构体,用到了几个C++11的新特性:auot、decltype、返回类型后置、declval和trype_traits。具体的实现原理是这样的:通过返回类型后置来推断表达式的类型,推断的过程中利用declval,它可以获取类型的右值引用,以便来调用成员函数,这个过程是在编译期完成的。如果通过declval调用成员函数成功了,则会继续推断逗号表达式的类型,最终推断的函数返回类型为std::true_type;如果通过declval调用成员函数失败了则推断失败,编译器会选择优先级最低的check(...)函数,它的返回类型为std::false_type。我们最后根据返回类型既可知道类型是否存在foo函数。
虽然C++98/03和C++11两种写法都可以推断出是否存在成员函数foo,但是当情况稍微发生一点变化时,就会出现一个问题,比如下面的代码。
struct MyStruct { int foo { } }; static_assert(has_member_foo<MyStruct>::value, "false"); //断言错误 static_assert(has_member_foo11<MyStruct>::value, "true");
当我们把foo的返回类型修改为int时,C++98/03版本的元函数就推断不出来,原因是因为我们定义辅助结构体时限定了函数签名template struct SFINAE {},这个只能实例化void类型的函数,对于返回类型为int的函数就无能为力了。但是对于C++11版本的元函数来说没有问题,因为C++11没有限定返回类型,declval不关心函数返回类型,它只是在编译期调用了成员函数。从这里可以看到C++11的实现方式更灵活,但是这个实现还不够通用,因为它只能推断不含参数的foo,如果存在含参的重载函数foo,该如何处理呢?这个问题对于C++98/03来说几乎是无法解决的一个问题,但对于C++11来说是很轻松的一件事情,通过C++11的新特性--可变模版参数即可解决。下面是通用版本的实现。
template<typename T, typename... Args> struct has_member_foo11 { private: template<typename U> static auto check(int) -> decltype(std::declval<U>.foo(std::declval<Args>...), std::true_type); template<typename U> static std::false_type check(...); public: enum { value = std::is_same<decltype(check<T>(0)), std::true_type>::value }; };
测试代码:
struct MyStruct { string foo { return ""; } int foo(int i) { return i; } }; static_assert(has_member_foo<MyStruct>::value, "true"); static_assert(has_member_foo<MyStruct, int>::value, "true");
虽然我们借助可变模版参数解决了重载函数的问题,但还有一个不足之处,它只能检查函数名为foo的类,如果需要检查其它函数名的类,则需要再定义一个几乎相同的类,这样做繁琐而低效。重复定义的问题我们可以借助宏来解决。下面是实现代码。
#define HAS_MEMBER(member)\ template<typename T, typename... Args>struct has_member_##member\ {\ private:\ template<typename U> static auto Check(int) -> decltype(std::declval<U>.member(std::declval<Args>...), std::true_type);\ template<typename U> static std::false_type Check(...);\ public:\ enum{value = std::is_same<decltype(Check<T>(0)), std::true_type>::value};\ };
这样每次需要检查不同函数名的时候只需要定义一个简单的宏即可,比如我们需要检查foo和func函数时,可以这样定义。
HAS_MEMBER(foo) HAS_MEMBER(func)
测试代码:
struct MyStruct { string foo { return ""; } int func(int i) { return i; } }; static_assert(has_member_foo<MyStruct>::value, "true"); static_assert(has_member_func<MyStruct, int>::value, "true");
借助C++11的新特性我们完美地解决了检查成员函数是否存在的问题,再回头来看文章开始提出的问题,我们实现了解决问题的第一步,第二步需要在第一步判断的基础上选择分支。同样借助C++11中的type_traits来实现编译期选择分支,具体是通过enable_if来实现的。
template<typename T> typename std::enable_if<has_member_foo11<T, int>::value>::type testfun(T t) { cout << "has foo" << endl; } template<typename T> typename std::enable_if<!has_member_foo11<T, int>::value>::type testfun(T t) { cout << "has no foo" << endl; }
最终我们借助C++11完美地解决了编译期检查成员函数是否存在并且在编译期选择分支的问题,实现方式相比C++98/03更加简洁、灵活和通用。
函数缓存
为了优化程序性能我们经常使用缓存,比如某个函数非常耗时,频繁调用的时候性能会很低,这时我们就可以通过缓存来提高性能。先看看下面的代码。
size_t fibonacci(size_t n) { return (n < 2) ? n : fibonacci(n - 1) + fibonacci(n - 2); } void test { Timer t; size_t n = fibonacci(45); cout << n << " " << t.elapsed << endl; }
这个代码在普通双核笔记本上耗时8秒多,这个函数刚好可以通过缓存来优化。
size_t fibonacci(size_t n) { return (n < 2) ? n : sugar(fibonacci2)(n - 1) + sugar(fibonacci2)(n - 2); }
优化之后的fibonacci相比之前的fibonacci2仅仅是多了一个sugar,然而性能却可以提高数十倍,因为sugar内部做了缓存,下面来看看sugar的具体实现。
template <typename R, typename... Args> std::function<R(Args...)> sugar(R(*func)(Args...), bool needClear = false) { using function_type = std::function<R(Args...)>; static std::unordered_map<decltype(func), function_type> functor_map; if (needClear) return functor_map[func] = cache(func); if (functor_map.find(func) == functor_map.end) functor_map[func] = cache(func); return functor_map[func]; }
sugar函数内部有一个map(注意,这里用的静态局部变量,C++11中的静态局部变量是线程安全的),用来保存被缓存函数的函数指针和一个std::function,std::function又是从cache函数返回出来的,接下来看看cache的实现。
template <typename R, typename... Args> std::function<R(Args...)> cache(R(*func) (Args...)) { auto result_map = std::make_shared<std::map<std::tuple<Args...>, R>>; return ([=](Args... args){ std::tuple<Args...> t(args...); if (result_map->find(t) == result_map->end) (*result_map)[t] = func(args...); return (*result_map)[t]; }); }
cache内部有一个map,它保存了被缓存函数调用时的入参和函数的执行结果。cache函数还定义了一个lambda表达式,用于延迟执行,在外面调用function时才执行。Lambda内部的逻辑很简单就是保存调用时的参数和执行结果,当下次调用时会先检查调用的参数是否已经存在,如果已经存在则从缓存map中返回,如果不存在才调用被缓存的函数,同时保存新的调用参数和调用结果。图1是fibonacci函数缓存的过程。
图1 fibonacci函数缓存的过程
C++11实现的这个函数缓存相比传统的缓存方法来说有下面几个优点:
1.相比传统的缓存实现方式,它更通用,可以适应任何函数,无需为每一个被缓存函数定义专门的缓存对象。
2.更简洁,只需要一个sugar既可以实现缓存。
实现一个http router
http router是web服务器的一个核心模块,它会解析url,然后路由到正确的handler上,下面是cinatra框架(一个现代C++写的web framework)的用法。
Cinatra app; app.route("/hello", (Request& req , Response& res) { res.end("Hello "); }); app.route("/hello/:name/:age", (Request& req, Response& res, const std::string& a, int b) { res.end("Name: " + a + ", Age: " + boost::lexical_cast<std::string>(b)); });
当用户在浏览器中输入http://127.0.0.1/hello时,会路由到第一个handler,当用户输入
http://127.0.0.1/hello/test/20时,会路由到第二个handler。Cinatra支持多种方式的请求,比如
http://127.0.0.1/hello/test/20这种请求,它是非标准的,但它可以方便用户使用,用户不必通过get参数的方式去请求了,直接将参数放到后面,使用起来很方便。但是这种方式的实现却很困难,实现这种方式的本质是字符串到函数调用的映射,如图2所示。
图2
如果想通过C++98/03来实现几乎是不可能完成的任务,但对于C++11来说却不是难题。
C++11的实现思路如下:
1. 解析url将参数分割,分割之后的字符串放到列表中{"hello", "test", "2"};
2. 将列表中的参数转换为function的实参;
2-1. 将列表中的参数分别转换为function的实参。
2-2. 将转换后的实参一个一个保存到“容器”--tuple中。
2-3. 将tuple转换为函数实参。
3. handler调用。
实现的关键在于如何将分割的字符串一个一个转换为handler的实参,转换之后又如何实现调用。将字符串转换为handler的实参借助了function_traits,它可以获取函数每一个参数的类型,知道这个类型之后我们就可以将字符串转换为对应类型的值了,即函数实参。转换之后还要考虑保存的问题,因为函数实参的类型是不同的,普通的容器是无法保存不同类型的数据的,这里借助tuple来保存类型不同的实参。保存实参之后还需要完成函数调用,这里需要通过可变模版参数和tuple相互转换来实现函数调用。下面来看看实现的关键技术。
【字符串转换为对应的函数实参】
通过function_traits来获取函数参数个数和每个参数的类型,获取之后就可以调用boost.lexicast来将字符串转换为对应的参数值了,接下来将转换之后的参数放到tuple中,以备用于在下一步的函数调用。下面是实现的关键代码,完整的代码读者可以参考我github上的代码:
https://github.com/kxjhcs/cosmos/blob/master/router.hpp。
template<typename Function, size_t N = 0, size_t M = function_traits<Function>::arity> struct invoker { template<typename Args> static inline void apply(const Function& func, token_parser & parser, Args const & args) { typedef typename function_traits<Function>::template args<N>::type arg_type; router::invoker<Function, N + 1, M>::apply(func, parser, std::tuple_cat(args, std::make_tuple(parser.get<arg_type>))); } }; template<typename Function, size_t M> struct invoker<Function, M, M> { template<typename Args> static inline void apply(const Function& func, token_parser &, Args const & args) { //参数列表已经准备好,可以调用function了 call(func, args); } };
【实现函数调用】
在前面我们已经将字符串转换为实参并保存到tuple中了,接下来我们需要将tuple转换为函数实参列表,这需要通过将tuple转换为可变模版参数,下面是实现的关键代码。
template<typename F, int ... Indexes, typename ... Args> static void call_helper(F f, IndexTuple<Indexes...>, const std::tuple<Args...>& tup) { f(std::get<Indexes>(tup)...); } template<typename F, typename ... Args> static void call(F f, const std::tuple<Args...>& tp) { call_helper(f, typename MakeIndexes<sizeof... (Args)>::type, tp); }
其中,MakeIndexes生成了一个整形序列,通过这个整形序列就可以将tuple转换为可变模板参数并完成函数调用了。MakeIndexes的具体实现读者可以参考github上的代码,其实在C++14的utility库中已经提供了std::index_sequence和std::make_index_sequence,代码会更加简洁。
C++11实现的http router不仅仅可以用于http服务器端的路由,还可以实现rpc和mvc框架的路由,所以用C++11去做web开发也是完全可以的。
总结
本文先通过介绍检查成员函数是否存在,比较了C++98/03和C++11的实现差异,C++11的实现无疑更加简洁和通用,完美地解决了问题,体现了现代C++解决问题的极致之美。接着介绍了如何实现一个通用的函数缓存,仅仅一个泛化的函数既可以实现缓存任意函数,体现了现代C++的简洁之美和泛化之美。最后展示了现代C++在做web开发的时候如何实现灵活的路由,完成了之前C++98/03几乎不可能完成的任务,体现了现代C++的威力,不仅仅是生产力的巨大提升。
现代C++仍然在发展完善当中,除了近年出现的C++11/14,未来的C++17/20标准正在规划制定当中。C++17将引入更多地激动人心的特性,有并行、并发、网络库、协程、modules和concepts等等,这将是C++发展史上的一座里程碑。
现代C++先天的性能优势与后天的效率(现代、快速开发)优势,除了传统的服务端、游戏、安全、嵌入式和音视频等领域外,用来做移动互联网、云平台和大数据分析正当其时。
作者简介
祁宇,珠海云创科技研发中心技术总监,资深C++技术专家。致力于C++11的应用、研究和推广。现负责公司云基础架构的研发。精通OOP、OOD、设计模式和重构,主要研究方向为架构设计和业务重构,有丰富的开发和研发管理经验。爱好C++,爱好开源,乐于研究和分享技术,开源了多个项目。
本文选自程序员电子版2015年12月A刊,该期更多文章请查看这里。2000年创刊至今所有文章目录请查看程序员封面秀。欢迎订阅程序员电子版(含iPad版、Android版、PDF版)。
相关推荐
- 微信又双叒叕更新了!这次是安卓版
-
澎湃新闻综合报道近日安卓版微信正式更新了8.0.10版主要有四大更新日常使用起来会更加方便一起来看看吧1朋友圈视频封面在此之前,朋友圈背景一直只能放静态图片,但此次更新后,可以从视频号中选择一段...
- 镜子里的你和照片里的你,哪个更真实?
-
不知道大家有没有这样的经历。聚餐、团建……一群人拍合照,拍完之后,我们满心期待地放大照片,却惊慌失措地发现——怎么自己又被拍得这么丑!但这时,别人总是会说道——「这就是你平常的样子啊。」可是,我们平时...
- 歼20战斗机现身珠海,首次公开静态展示,体现解放军的自信和强大
-
日本航空自卫队在9月份举行了三泽基地开发日活动,期间出动12架F-35A闪电II战斗机进行了公开展示,不过仅仅是编队通场飞过而已。日本航空自卫队仅仅动用1架F-35A战斗机进行了机动飞行表演,从公开的...
- Java类初始化阶段深度解析:执行顺序与线程安全
-
一、初始化阶段核心机制二、分步详解与代码验证1.初始化触发条件主动使用场景:publicclassInitTrigger{static{System.out.pr...
- 深入剖析 Java 类加载机制:原理、优化与实践
-
作为Java开发者,你是否遇到过这样的场景:线上服务突然抛出NoClassDefFoundError,但本地调试却一切正常;或者明明引入了依赖JAR,却始终报ClassNotFoundExcep...
- SUID/SGID是啥?如何让普通用户拥有root的能力?
-
原文链接:「链接」在Linux系统中,权限控制是一项至关重要的安全机制。除了常见的r(读)、w(写)和x(执行)权限外,还有三种特殊权限位常被忽视:SUID(SetUserID)、SGID...
- 数码宝贝新世纪:SP奥米加兽AS情报泄露,是否也是强力辅助?
-
大家好!我是小飉[liáo],欢迎来阅!情怀手游《数码宝贝新世纪》官方不按套路出牌,这次公布的入围测试的人员名单,但是并没有公布SP奥米加兽AS的能力情报,还好广大网友给力。次日,在论坛,以及...
- 抽象类(abstract class)与接口(interface)
-
A.核心概念1.抽象类-定义:带有abstract修饰符的类,不能被实例化,用于定义一组方法签名和可选的部分公共实现。-特性:-可以包含字段、构造函数、已实现的方法(带方法体)和抽象方法(...
- S39结束时间确定,新赛季段位继承公布,大量皮肤在7月初集体上线
-
文/静海君如果说之前都还是猜测的话,那游戏内的一个变动,基本100%确定了新赛季(S40)的开启时间。新赛季的开启时间关于新赛季的开启时间,目前主要有两个线索。第一个关于新赛季开启时间的线索是「游戏内...
- 一篇文章掌握整个JVM,JVM超详细解析!!!
-
不懂JVM看完这一篇文章你就会非常懂了,文章很长,非常详细!!!先想想一些问题1我们开发人员编写的Java代码是怎么让电脑认识的首先先了解电脑是二进制的系统,他只认识01010101比如我们经常要...
- 项目用 JDK17 后,bug 少了、速度快了!这 4 个好处太实在
-
别再死守JDK8了!去年把电商项目升级到JDK17,团队直接爽翻:代码量少写1/3,大促再也不卡顿,运维半夜不call人,连测试都夸bug少了。今天就说真话,JDK17在项目里的4...
- 法定继承有顺序:在法定继承人中,谁应该优先继承?
-
免费问律师_法律咨询免费24小时律师在线解答-法临网“父母去世没留遗嘱,兄弟姐妹争遗产闹上法庭!”法定继承中,谁优先拿财产?《民法典》明确“顺序+份额”规则,一文说清关键点,避免家庭内耗!一、法定...
- 前端必会:ES5寄生继承 vs ES6 Class继承
-
大家好,我是谦!说到继承,估计不少前端开发者都踩过坑。尤其是在ES5到ES6的过渡阶段,我们写代码时常常被问到:“你用的是原型继承还是Class继承?”再加上面试官特别喜欢追问底层实现——...
- 子女入了外籍能否继承父母国内的房产呢?
-
大家好,这里是家理范律,专注遗产继承、婚姻家事领域!-很多加入外籍的朋友都纠结:自己还能继承国内父母的房产吗?答案是可以继承,但流程远比想象复杂!-真实案例:美籍华人张先生,拿着父母在加州公证的遗嘱回...
- J.A.C.S | 基于化学类型和靶点的基因组挖掘以寻找一种新的细菌肽脱甲酰酶天然产物抑制剂
-
大家好,今天推送的文章是2025年6月发表在JournaloftheAmericanChemicalSociety上的“Chemotype-andTarget-DrivenGenome...
- 一周热门
- 最近发表
- 标签列表
-
- HTML 简介 (30)
- HTML 响应式设计 (31)
- HTML URL 编码 (32)
- HTML Web 服务器 (31)
- HTML 表单属性 (32)
- HTML 音频 (31)
- HTML5 支持 (33)
- HTML API (36)
- HTML 总结 (32)
- HTML 全局属性 (32)
- HTML 事件 (31)
- HTML 画布 (32)
- HTTP 方法 (30)
- 键盘快捷键 (30)
- CSS 语法 (35)
- CSS 轮廓宽度 (31)
- CSS 谷歌字体 (33)
- CSS 链接 (31)
- CSS 定位 (31)
- CSS 图片库 (32)
- CSS 图像精灵 (31)
- SVG 文本 (32)
- 时钟启动 (33)
- HTML 游戏 (34)
- JS Loop For (32)