• C++模板编程:如何使非通用的模板函数实现声明和定义分离


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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

    #include "Math.h"
    
    template <class _Type>
    _Type Math::log2(float value) {
        return 0;
    }
    
    template int Math::log2<int>(float value); 
    template float Math::log2<float>(float value);

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

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

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

    std::string resultString = Math::log2<std::string>(1000.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@2@@std@@@Math@@SA?AV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@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 0;
    }
    
    template <>
    int Math::log2<int>(float value) {
        assert(value > 0.);
        assert(sizeof(value) == sizeof(int));
        assert(sizeof(value) == 4);
        return (((*(int *)&value)&0x7f800000)>>23)-0x7f;
    }
    
    template <>
    float Math::log2<float>(float value) {
        assert(value > 0.);
        assert(sizeof(value) == sizeof(int));
        assert(sizeof(value) == 4);
        int i = (*(int *)&value);
        return (((i&0x7f800000)>>23)-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 0;
    }

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

    现在一个接近完美的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中填写的内容。

     


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

     
  • 相关阅读:
    UML中几种类间关系:继承、实现、依赖、关联、聚合、组合的联系与区别
    使用Unity extension 设置默认的拦截器(interceptor)
    修复Eclipse debug时提示‘Cannot connect to VM’
    Windows下删除.svn文件夹的最简易方法
    Collections的copy()方法和ArrayList的大小问题
    .NET Framework 3.5中序列化成JSON数据及JSON数据的反序列化,以及jQuery的调用JSON
    【设备编程】海康视频监控设备C#二次开发系列一
    【Asp.Net】自定义控件?用户控件?还是新型的复合控件?
    windows phone 获取udid
    windows phone 如何获得手机的分辨率
  • 原文地址:https://www.cnblogs.com/aanbpsd/p/Viola_cpp_log2.html
Copyright © 2020-2023  润新知