引子

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

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. SpringMVC multipart文件上传

    一.介绍   spring内建的multipart支持网络程序文件上传.我们可以通过配置MultipartResolver来启动上传支持.它定义在org.springframework.web.mul ...

  2. 聚集函数查询结果为空, list的size是1, resolve

    resultList.removeAll(Collections.singleton(null));

  3. ReactiveCocoa 中 RACSignal 是怎样发送信号

    前言 ReactiveCocoa是一个(第一个?)将函数响应式编程范例带入Objective-C的开源库.ReactiveCocoa是由Josh Abernathy和Justin Spahr-Summ ...

  4. C语言学习笔记 (001) - 常量指针与指针常量的区别(转帖)

    三个名词虽然非常绕嘴,不过说的非常准确.用中国话的语义分析就可以很方便地把三个概念区分开. 一) 常量指针. 常量是形容词,指针是名词,以指针为中心的一个偏正结构短语.这样看,常量指针本质是指针,常量 ...

  5. 安装 Linuxbrew

    在 OS X 平台上非常流行的包管理器 Homebrew 最近正被移植到 Linux 上而成为 Linuxbrew.虽然各种 Linux 发行都带有自己的包管理工具,诸如 apt-get.yum.pa ...

  6. 【HTML】 向网页<Title></Title>中插入图片以及跑马灯

    <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <style typ ...

  7. PWA 入门: 写个非常简单的 PWA 页面

    Progressive Web Apps 是 Google 提出的用前沿的 Web 技术为网页提供 App 般使用体验的一系列方案. 这篇文章里我们来完成一个非常简单的 PWA 页面. 一个 PWA ...

  8. Linux 性能測试工具

    Linux 性能測试工具 linux performance 查看系统配置 查看CPU信息 lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64- ...

  9. appium简明教程(8)——那些工具

    那片笑声让我想起我的那些tool 在我生命每个角落静静为我开着 我曾以为我会永远守在她身旁 今天我们已经离去在人海茫茫 她们都老了吧 都更新换代了吧 幸运的是我曾陪她们开发 啦…… 想她 啦…… 她还 ...

  10. Java String首字母大写

    一種寫法參考 public String upperFirstChar(String input) { if (input == null || "".equals(input)) ...