引子

  前些时候,我在群里出了一道题目:将变参的类型连接在一起作为字符串并返回出来,要求只用函数实现,不能借助于结构体实现。用结构体来实现比较简单:

template<typename... Args> struct Connect;

template< typename First, typename... Rest>
struct Connect<First, Rest...>
{
static string GetName()
{
return typeid(First).name() + string(" ") + Connect<Rest...>::GetName();
}
}; template<typename Last> struct Connect<Last>
{
static string GetName()
{
return typeid(Last).name();
}
};

测试代码:

auto str = Connect<int, double>::GetName();
//str为int double

如果改成函数的话,试着这样写:

template<typename T>
string GetNameofTypes()
{
return typeid(T).name();
} template<typename T, typename... Rest>
string GetNameofTypes()
{
string str += GetNameofTypes<T>() + " " + GetNameofTypes<Rest...>(); return str;
}

很遗憾,这样是编译不过的,因为编译器不知道选择哪个。有两个方法可以解决这个问题,这里主要来介绍通过逗号表达式来解决这个问题。

逗号表达式和变参的相逢

  前段时间播放的舌尖上的中国第二季中有一集为相逢,当南北不同风味的普通食材放到一起时,立刻化腐朽为神奇变成难得的美味了,这正是食材相逢组合而成的天作之合。那么在c++中,不同的特性相逢在一起又是什么滋味呢?一定很奇妙。下面来看看舌尖上的c++中两个普通食材吧。

食材一:逗号表达式

  我们知道逗号表达式会按顺序执行逗号前面的表达式,比如:
d = (a = b, c);
  这个表达式会按顺序执行的:a先会被赋值b,接着d会被赋值c。

食材二:可变参数模板

c++11的可变参数模板增强了模板功能,在c++11之前,类模板和函数模板只能含有固定数量的模板参数,现在c++11中的新特性可变参数模板允许模板定义中包含0到任意个模板参数。可变参数模板和普通模板的语义是一样的,只是写法上稍有区别,可变参数模板声明时需要在typename或class后面带上省略号“...”。
省略号(...)的作用有两个

  • 声明一个参数包,这个参数包中可以包含0到任意个模板参数;
  • 在模板定义的右边,可以将参数包展开成一个一个独立的参数。

逗号表达式和变参的相逢

  来看看逗号表达式和变参的相逢会产生什么奇妙的效果。

template <class T>
void printarg(T t)
{
cout << t << endl;
} template <class ...Args>
void expand(Args... args)
{
int arr[] = {(printarg(args), )...};
  cout<<sizeof(arr)<<endl;
}

测试代码:

expand(,,,);
//将输出1 2 3 4

  上面的例子将分别打印出1,2,3,4,将可变参数模板就地展开了。是的,是通过一个没有引用的数组来就地展开的,看到这种写法不要惊讶,这就是它们相逢产生的神奇效果,独有的味道。
  我们来看看这种奇妙的效果是如何产生的:

  我们知道逗号表达式会按顺序执行逗号前面的表达式,比如:
  d = (a = b, c);
  这个表达式会按顺序执行的:a先会被赋值b,接着d会被赋值c。
  expand函数中的逗号表达式:(printarg(args), 0),也是按照这个执行顺序,是先执行printarg(args),再得到逗号表达式的结果0。同时还用到了c++11的另外一个特性:初始化列表,通过初始化列表来初始化一个变长数组, {(printarg(args), 0)...}将会展开成((printarg(arg1),0), (printarg(arg2),0),printarg(arg3),0), etc... );最终会创建一个元素值都为0的数组int arr[sizeof...(Args)],由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。

  再回过头来说说本文开头提到的那个问题,通过函数将变参类型作为字符串返回出来。通过逗号表达式可以很轻松完成这个任务:

template<typename T>
string GetName()
{
return typeid(T).name();
} template<typename... Rest>
string GetNameofTypes()
{
string str;
int arr[] = { (str +=" "+ GetName<Rest>(), )... };
   cout<<sizeof(arr)<<endl;
return str;
} //测试代码:
auto s = GetNameofTypes<int, double, char>();
//s将为 int double char

  尝了上面的逗号表达式和可变参数模板的相逢产生奇妙的味道,一定还在回味之中,意犹未尽吧。我还没说其实还有一样食材没说呢,如果将逗号表达式和这种食材相逢在一起便又是另外一种独特的味道了。再来看看另外一种食材吧。

食材三:decltype表达式类型推断

  C++11新增了decltype关键字,用来在编译时推导出一个表达式的类型。它的语法格式如下:
decltype(exp)
  其中exp表示一个表达式(expression)。
它可以用来推导表达式标示符和表达式的类型,比如:

const int bar();
int i;
struct A { double x; };
const A* a = new A();
decltype(bar()) x2; // 类型为const int
decltype(i) x3; // 类型为int
decltype(a->x) x4; // 类型为double
decltype((a->x)) x5; // 类型为const double&

  我们一般用decltype来推断函数的返回类型,和auto结合起来,组成一种返回值类型后置的语法,比如下面的例子:

template <typename T, typename U>
auto add(T t, U u) -> decltype(t + u)
{
return t + u;
}

  为什么要返回类型后置呢?因为返回类型要依赖于模板参数,如果将decltype(t + u)放到函数的前面则无法编译通过,因为在定义函数返回值的时候,模板参数变量都还不存在呢。所以就借助auto来将返回类型占位住,等decltype推导之后再给auto初始化,从而获取了函数的返回值。

逗号表达式和decltype的相逢

  来看看逗号表达式和decltype的相逢又会产生什么奇妙的效果。比如有这样一个需求,我需要在编译期判断某个类是否存在void Reserve(int i)函数。

template<typename T>
struct Has_Reserve_Method
{
private:
typedef std::true_type True;
typedef std::false_type False; template<typename U> static auto Check(int) -> decltype(std::declval<U>().Reserve (), True()); template<typename> static False Check(...); public:
enum
{
value = std::is_same<decltype(Check<T>()), True>::value
};
}; struct AA
{
void Reserve(int i)
{
cout << "ok" << endl;
}
}; 测试代码:
if (Has_Reserve_Method<AA>::value)
cout << "ok" << endl; //将输出OK

  这里利用了SFINAE特性,它的全称是Substitution failure is not an error,匹配失败并不是错误,意思是用函数模板匹配规则来判断类型的某个属性是否存在,也就是说SFINAE可以作为一种编译期的不完整内省方法。

template<typename U> static auto Check(int) -> decltype(std::declval<U>().Reserve(), True());
template<typename> static False Check(...);

  这两行代码是关键,当Check匹配不上时,返回False;当匹配上之后,通过逗号表达式返回True,这样外面就可以根据True和False来检查是否存在该函数了。

  怎么样这道菜的味道也相当好吧。其实还有很多c++特性的相逢产生的奇妙味道我们还没有发现,正等待着我们去发现呢。

后记

  由于typeid可能会丢失一些类型信息,要获取准确的类型名称还需要做一些专门的处理,这里为了简单起见,忽略了typeid获取类型名可能不准确的影响。另外,有童孩说逗号表达式来展开变参的代码可读性很差,这里我也不推荐大家在实际的代码中也这样写,还是老老实实的用结构体来展开变参吧,其实通过函数来展开变参(不用逗号表达式)还有种写法:

string GetNameofMsgType()
{
return "";
} template <typename First>
string GetNameofMsgType()
{
return std::string(typeid(First).name());
} template <typename First, typename Second, typename... Args>
string GetNameofMsgType()
{
return GetNameofMsgType<First>() + " " + GetNameofMsgType<Second, Args...>();
}

如果你觉得这篇文章对你有用,可以点一下推荐,谢谢。

c++11 boost技术交流群:296561497,欢迎大家来交流技术。

(原创)舌尖上的c++--相逢的更多相关文章

  1. BZOJ_4867_[Ynoi2017]舌尖上的由乃_分块+dfs序

    BZOJ_4867_[Ynoi2017]舌尖上的由乃_分块+dfs序 Description 由乃为了吃到最传统最纯净的美食,决定亲自开垦一片菜园.现有一片空地,由乃已经规划n个地点准备种上蔬菜.最新 ...

  2. 舌尖上的硬件:CPU/GPU芯片制造解析(高清)(组图)

    一沙一世界,一树一菩提,我们这个世界的深邃全部蕴藏于一个个普通的平凡当中.小小的厨房所容纳的不仅仅是人们对味道的情感,更有推动整个世界前进的动力.要想理解我们的世界,有的时候只需要细细品味一下我们所喜 ...

  3. [原创]gerrit上分支操作记录(创建分支、删除分支)

    Git分支对于一个项目的代码管理而言,是十分重要的! 许多久用git的朋友可能已经掌握的很牢固了,但对于一些初涉git的童鞋来说,可能还不是很熟悉. 在此,我将自己的一些操作经历做一梳理,希望能帮助到 ...

  4. BZOJ4867 : [Ynoi2017]舌尖上的由乃

    首先通过DFS序将原问题转化为序列上区间加.询问区间kth的问题. 考虑分块,设块大小为$K$,每块维护排序过后的$pair(值,编号)$. 对于修改,整块的部分可以直接打标记,而零碎的两块因为本来有 ...

  5. 舌尖上的javascript数组和字符串基本操作

    Javascript数组基本操作 Javascript中的数组是一种特殊的对象,用来表示偏移量的索引是该对象的属性,索引可能是整数,然而这些数字索引在内部被转换为字符串类型,这是因为javascrip ...

  6. BZOJ4867 Ynoi2017舌尖上的由乃(dfs序+分块)

    容易想到用dfs序转化为序列上的问题.考虑分块,对每块排序,修改时对于整块打上标记,边界暴力重构排序数组,询问时二分答案,这样k=sqrt(nlogn)时取最优复杂度nsqrt(nlogn)logn, ...

  7. [原创] Debian9上配置Samba

    Samba概述 Samba是一套使用SMB(Server Message Block)协议的应用程序,通过支持这个协议,Samba允许Linux服务器与Windows系统之间进行通信,使跨平台的互访成 ...

  8. 原创:史上对BM25模型最全面最深刻的解读以及lucene排序深入讲解

    垂直搜索结果的优化包括对搜索结果的控制和排序优化两方面,其中排序又是重中之重.本文将全面深入探讨垂直搜索的排序模型的演化过程,最后推导出BM25模型的排序.然后将演示如何修改lucene的排序源代码, ...

  9. 【原创+史上最全】Nginx+ffmpeg实现流媒体直播点播系统

    #centos6.6安装搭建nginx+ffmpeg流媒体服务器 #此系统实现了视频文件的直播及缓存点播,并支持移动端播放(支持Apple和Android端) #系统需要自行安装,流媒体服务器配置完成 ...

随机推荐

  1. MySQL的binlog操作

    1. MySQL的binlog有三种模式: statement, row and mixed, 从5.1开始支持row, 默认是row模式 2. 设置参数 # 要配置在mysqld下 [mysqld] ...

  2. TL 重构

    import dependencies.*;import org.apache.commons.lang3.ArrayUtils;import org.apache.commons.lang3.Str ...

  3. QQ 互联认证 回调地址提示说要http :// 但是事实不用

    真奇怪 腾讯最近人手不够吧 这样的错误也会犯错....

  4. 使用Dockerfile文件构建基于centOS系统的nodejs镜像

    实际示例: [root@node01 node]# ls dev-web dev-web.tar.gz Dockerfile node-v8.14.0-linux-x64.tar.gz package ...

  5. Jetty使用内存过大的解决方案

    之前用Jetty做过一个消息通知服务器,主要功能就是其他各个子系统如果有需要push给客户端消息的就把这个消息发给我的Server,我用WebSocket来推送给客户端~ 程序上线一段时间之后运维工程 ...

  6. 树莓派进阶之路 (029) - 语音识别模块 LD3320(原创)

    近几天听朋友有说到LD3320 语音模块,刚好身边有块树莓派3,就在某宝上买了块自带mcu的LD3320 . 准备: 树莓派一个(配置了wiringPi开发环境的详情见本人博客:树莓派进阶之路 (00 ...

  7. HDU 1258 Sum It Up (DFS)

    Sum It Up Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others)Total S ...

  8. easyui-linkbutton 设置和获取text文本

    <a id="butTransagt"  href="#" class="easyui-linkbutton"  icon=" ...

  9. C# 连接 mySQL 出现 GUID 应包含带 4 个短划线的 32 位数 问题

    C# 连接 mySQL 出现 GUID 应包含带 4 个短划线的 32 位数 问题 在连接字符串中加入 Old Guids=true; 如:server=localhost;userid=root;p ...

  10. FreeSWITCH在会议室中持续播放音频文件

    最近遇到一个客户需求,希望在会议室建立起来后,自动播放一段指定的声音. 已知会议室命令,假设建立起一个会议室号码3000,很容易实现以下功能: 一.播放一个声音文件一次 conference 3000 ...