我们在编写C++类库时,为了隐藏实现,往往只能忍痛舍弃模版的强大特性。但如果我们只需要有限的几个类型的模版实现,并且不允许用户传入其他类型时,我们就可以将实例化的代码放在cpp文件中实现了。然而,当我们又需要针对特定类型进行模版偏特化时,由于gcc编译器不允许直接在类中进行偏特化声明,所以正确的写法变得比较复杂。本文通过一个简单的求log2函数的例子,提供了一个在cpp中同时进行偏特化和实例化的一般写法,并且可以使用static_assert在编译期检查参数的实现。


现在假设我们有一个叫做"Math"的工具类,它的所有操作都以public静态函数提供。现在我们要添加一个log2函数,并且这个函数的有两个版本:

    int log2(float value);
float log2(float value);

我们知道在C++中函数重载必须要有不同的参数列表,直接这样声明肯定是不行的。但我们又不想在函数名中加上返回类型,此时我们很自然地想到了使用模版。这样我们期望的使用方法如下:

    int resultInt = Math::log2<int>(.f);
float resultFloat = Math::log2<float>(.f);

(PS. 这个例子仅作讲解之用,至于是否要做成模版参数的样式仍需按照项目的规范来设计。)

下面我们先给log2函数附上一个不正确的实现,让我们的例子能够编译并运转起来。

头文件Math.h中的代码如下:

class Math {
public:
template <class _Type>
static _Type log2(float value) {
return ;
}
};

在main.cpp中,加入我们的测试代码:

int main() {
int resultInt = Math::log2<int>(.f);
float resultFloat = Math::log2<float>(.f);
printf("resultInt = %d\n", resultInt);
printf("resultFloat = %f\n", resultFloat);
return ;
}

编译并运行,可以看到控制台上输出的resultInt和resultFloat都是0。

现在这个库函数可以使用了。但所有人都可以在math.h中查看到我们所有的实现代码,如果我们想要隐蔽实现,就需要想办法把它们放到cpp文件中。

我们注意到log2函数的返回值只能是int或float,返回其他自定义类型都是没有意义的,因此我们不必让所有编译单元都看到函数的实现体,只需要在cpp中定义一个实现,再在实现体之后显示装载模版类即可。

现在创建一个math.cpp,把我们代码的实现部分移过去。

#include "Math.h"

template <class _Type>
_Type Math::log2(float value) {
return ;
} template int Math::log2<int>(float value);
template float Math::log2<float>(float value);

编译并运行,我们仍得到了方才一致的结果。

将实现放在cpp中不仅起到了隐蔽实现的功能,同时还获得了一个额外的好处,就是一旦用户使用了错误的类型实例化,就会引发链接错误。

倘若我们在main函数中添加一句:

std::string resultString = Math::log2<std::string>(.f);

再次编译,我们会看到链接器报错:

无法解析的外部符号 "public: static class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > __cdecl Math::log2<class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > >(float)" (??$log2@V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@@@std@@@Math@@SA?AV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@@@std@@M@Z)

(VS2013)

把这一句注释掉(下面还会用到),我们开始为log2添加正确的实现。

先访问 http://www.musicdsp.org/showone.php?id=91 这里有一个非常快速的log2算法(当然不是十分精确),但是想要直接使用这些代码时我们却遇到了难题:这个算法返回int的函数floorOfLn2和返回float的函数approxLn2是完全不同的两个实现,然而我们的模版函数只有一个!要解决这个问题,需要针对int和float类型编写偏特化实现。

这里有一个值得注意的地方:我们在Visual Studio编写模版偏特化时,可以直接将偏特化声明直接放在类的定义部分。然而这在gcc中是不能通过编译的。想要我们的代码跨平台,就要遵循标准写法,将偏特化实现放在类外。我们的例子实现的代码已经在cpp中了,因此我们可以直接在cpp中添加偏特化的代码。

修改后的math.cpp如下:

#include "Math.h"

template <class _Type>
_Type Math::log2(float value) {
return ;
} template <>
int Math::log2<int>(float value) {
assert(value > .);
assert(sizeof(value) == sizeof(int));
assert(sizeof(value) == );
return (((*(int *)&value)&0x7f800000)>>)-0x7f;
} template <>
float Math::log2<float>(float value) {
assert(value > .);
assert(sizeof(value) == sizeof(int));
assert(sizeof(value) == );
int i = (*(int *)&value);
return (((i&0x7f800000)>>)-0x7f)+(i&0x007fffff)/(float)0x800000;
} template int Math::log2<int>(float value);
template float Math::log2<float>(float value);

编译运行,我们看到程序已经输出了正确的结果:

resultInt = 9
resultFloat = 9.953125

另外我们注意到,模版函数的那个一般实现:

template <class _Type>
_Type Math::log2(float value) {
return ;
}

已经不再使用了,我们可以将它删除,完全不会造成任何影响。

现在一个接近完美的log2函数就已经制作完毕了。

那么它的不足之处在哪里?我们再回过头观察math.h,发现函数的声明只有一个光秃秃的 template <class _Type> static _Type log2(float value)。对于用户来说,仅看到这一个头文件,是完全想象不到函数还有一个返回值的限定的。如果他不小心使用了错误的类型,IDE只会给出一个极不友好的链接错误,这样用户肯定就抓狂了。

想要提供一个友好的错误提示,可以考虑使用C++11引入的新特性static_assert,它可以帮我们检测出错误的类型,并在编译时就发现它们。

在编译期判断类型是否相同,我们需要引入一些type_traits。

定义如下:

struct TrueType {
static const bool value = true;
}; struct FalseType {
static const bool value = false;
}; template <class _A, class _B>
struct IsSameType : FalseType {
}; template <class _A>
struct IsSameType<_A, _A> : TrueType {
};

接下来我们将原来的log函数包装起来,放在private修饰符下,并改名为_log (不要忘记同时修改math.cpp中的符号)。

再新建一个一模一样的log函数,我们要做的就是在这个函数中写入static_assert检查模版参数类型,如果类型无误,我们才会调用真正的_log2函数。

修改后的math.h如下:

struct TrueType {
static const bool value = true;
}; struct FalseType {
static const bool value = false;
}; template <class _A, class _B>
struct IsSameType : FalseType {
}; template <class _A>
struct IsSameType<_A, _A> : TrueType {
}; class Math {
public:
template <class _Type>
static _Type log2(float value) {
static_assert(IsSameType<_Type, int>::value || IsSameType<_Type, float>::value,
"template argument must be int or float");
return _log2<_Type>(value);
} private:
template <class _Type>
static _Type _log2(float value);
};

把之前注释掉的传入std::string类型的那条语句恢复,再次编译,我们会看到这次报的是编译错误了,内容就是我们在static_assert中填写的内容。


本文由 哈萨雅琪 原创,转载请注明出处。

 

C++模板编程:如何使非通用的模板函数实现声明和定义分离的更多相关文章

  1. 【非原】c语言之声明和定义的区别

    原创地址:http://www.cnblogs.com/haore147/p/3647466.html 什么是定义?什么是声明?它们有何区别? 举个例子: 1 2 A)int i; B)extern  ...

  2. 使用 c++ 模板显示实例化解决模板函数声明与实现分离的问题

    问题背景 开始正文之前,做一些背景铺垫,方便读者了解我的工程需求.我的项目是一个客户端消息分发中心,在连接上消息后台后,后台会不定时的给我推送一些消息,我再将它们转发给本机的其它桌面产品去做显示.后台 ...

  3. C++中的内联成员函数与非内联成员函数

    在C++中内联成员函数与非内联成员函数的可以分为两种情况: 1.如果成员函数的声明和定义是在一起的,那么无论有没有写inline这个成员函数都是内联的,如下: using namespace std; ...

  4. C++模板编程-模板基础重点

    模板基础 1.模板参数自动推导,如果是已知的参数类型与个数,这调用模板时可以不写类型. Cout<<max<int>(1,3);可以写为Cout<<max(1,3) ...

  5. C++模板编程与宏编程经验谈

    C++模板编程与宏编程经验谈 有人说C 与C++的不同主要是因为C++支持模板,不要说区别是面向对象化编程,因为C同样能很好的实现对象化编程,面向对象化其实只是思想,在很多语言中都能实现,区别在于实现 ...

  6. C++之模板编程

    当我们越来越多的使用C++的特性, 将越来越多的问题和事物抽象成对象时, 我们不难发现:很多对象都具有共性. 比如 数值可以增加.减少:字符串也可以增加减少. 它们的动作是相似的, 只是对象的类型不同 ...

  7. c++ 基于Policy 的 模板编程

    在没真正接触c++  模板编程之前.真的没有想到c++ 还能够这么用.最大的感触是:太灵活了,太强大了. 最初接触模板威力还是在Delta3d中,感觉里面的模板使用实在是灵活与方便,特别是dtAI中使 ...

  8. 【C++编程基础】(1)—— 函数原型声明、函数模板、引用、const 常引用、const 常量指针

    一.函数原型声明: 1.函数声明告诉编译器函数的名称,和如何调用函数(返回类型和参数):函数定义提供了函数的实际主体. 2.强制性的:在C++中,如果函数调用的位置在函数定义之前,则要求在函数调用之前 ...

  9. C#模板编程(2): 编写C#预处理器,让模板来的再自然一点

    在<C#模板编程(1):有了泛型,为什么还需要模板?>文中,指出了C#泛型的局限性,为了突破这个局限性,我们需要模板编程.但是,C#语法以及IDE均不支持C#模板编程,怎么办呢?自己动手, ...

随机推荐

  1. JavaScript之父Brendan Eich,Clojure 创建者Rich Hickey,Python创建者Van Rossum等编程大牛对程序员的职业建议

    软件开发是现时很火的职业.据美国劳动局发布的一项统计数据显示,从2014年至2024年,美国就业市场对开发人员的需求量将增长17%,而这个增长率比起所有职业的平均需求量高出了7%.很多人年轻人会选择编 ...

  2. 【.net 深呼吸】设置序列化中的最大数据量

    欢迎收看本期的<老周吹牛>节目,由于剧组严重缺钱,故本节目无视频无声音.好,先看下面一个类声明. [DataContract] public class DemoObject { [Dat ...

  3. 快速构建H5单页面切换骨架

    在Web App和Hybrid App横行的时代,为了拥有更好的用户体验,单页面应用顺势而生,单页面应用简称`SPA`,即Single Page Application,就是只有一个HTML页面的应用 ...

  4. 浅谈 jQuery 核心架构设计

    jQuery对于大家而言并不陌生,因此关于它是什么以及它的作用,在这里我就不多言了,而本篇文章的目的是想通过对源码简单的分析来讨论 jQuery 的核心架构设计,以及jQuery 是如何利用javas ...

  5. FREERTOS 手册阅读笔记

    郑重声明,版权所有! 转载需说明. FREERTOS堆栈大小的单位是word,不是byte. 根据处理器架构优化系统的任务优先级不能超过32,If the architecture optimized ...

  6. 转:聊聊mavenCenter和JCenter

    Gradle支持从maven中央仓库和JCenter上获取构件,那这两者有什么区别呢? maven中央仓库(http://repo1.maven.org/maven2/)是由Sonatype公司提供的 ...

  7. 用游标实现查询当前服务器所有数据库所有表的SQL

    declare @name varchar(100) DECLARE My_Cursor CURSOR --定义游标 FOR (SELECT Name FROM Master..SysDatabase ...

  8. 【定有惊喜】android程序员如何做自己的API接口?php与android的良好交互(附环境搭建),让前端数据动起来~

    一.写在前面 web开发有前端和后端之分,其实android还是有前端和后端之分.android开发就相当于手机app的前端,一般都是php+android或者jsp+android开发.androi ...

  9. 前端性能优化的另一种方式——HTTP2.0

    最近在读一本书叫<web性能权威指南>谷歌公司高性能团队核心成员的权威之作. 一直听说HTTP2.0,对此也仅仅是耳闻,没有具体研读过,这次正好有两个篇章,分别讲HTTP1.1和HTTP2 ...

  10. 技术笔记:XMPP之openfire+spark+smack

    在即时通信这个领域目前只找到一个XMPP协议,在其协议基础上还是有许多成熟的产品,而且是开源的.所以还是想在这个领域多多了解一下. XMPP协议:具体的概念我就不写了,毕竟这东西网上到处是.简单的说就 ...