深入了解C++中基于模板的类型擦除

 更新时间:2023年12月13日 15:14:39   作者:拉普拉斯妖kk  
在C\C++中主要有三种类型擦除的方式:基于void*的类型擦除、面向对象的类型擦除和基于模板的类型擦除,本文主要为大家详细介绍基于模板的类型擦除的相关知识,需要的可以了解下

在C\C++中主要有三种类型擦除的方式:

基于void*的类型擦除,如C标准库的qsort函数。这中用法在C中是常见的。但因为是通过void*来操作数据,所以存在类型不安全的问题。

  • 函数原型:void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *))
  • 用途:对数组进行排序
  • 类型擦除:base 是一个指向数组元素的指针,其类型为 void*。这使得 qsort 可以处理任何类型的数组。

面向对象的类型擦除,也就是C++中的继承,通过父类的引用或指针来调用子类的接口。这样解决了void*的类型不安全问题,但是继承也带来了代码复杂度提升,以及侵入式设计的问题(子类的实现比如知道父类和其继承体系)。

基于模板的类型擦除,技术上来说,是编写一个类,它提供模板的构造函数和非虚函数接口提供功能;隐藏了对象的具体类型,但保留其行为。典型的就是std::function。

下面是一个示例代码,实现了通用的任务,这些任务可以是任意的函数对象。

#include <iostream>
#include <memory>

// 抽象基类TaskBase作为公共接口不变;其子类TaskModel写成类模板的形式,其把一个任意类型F的函数对象function_作为数据成员。
struct TaskBase
{
    virtual ~TaskBase() {}
    virtual void operator()() const = 0;
};

template <typename F>
struct TaskModel : public TaskBase
{
    F functor_;

    template <typename U> // 构造函数是函数模板
    TaskModel(U &&f) : functor_(std::forward<U>(f))
    {
    }

    void operator()() const override
    {
        functor_();
    }
};

// 对TaskModel的封装
class MyTask
{
    std::unique_ptr<TaskBase> ptr_;

public:
    template <typename F>
    MyTask(F &&f)
    {
        using ModelType = TaskModel<F>;
        ptr_ = std::make_unique<ModelType>(std::forward<F>(f));
    }

    void operator()() const
    {
        ptr_->operator()();
    }
};

/////////////测试代码/////////////////

// 普通函数
void func1()
{
    std::cout << "type erasure 1" << std::endl;
}

// 重载括号运算符的类
struct func2
{
    void operator()() const
    {
        std::cout << "type erasure 2" << std::endl;
    }
};
int main()
{
    // 普通函数
    MyTask t1{&func1};
    t1(); // 输出"type erasure 1"

    // 重载括号运算符的类
    MyTask t2{func2{}};
    t2(); // 输出"type erasure 2"

    // Lambda
    MyTask t3{
        []()
        { std::cout << "type erasure 3" << std::endl; }};
    t3(); // 输出"type erasure 3"

    return 0;
}

总结下,要实现基于模板的类型擦除主要有三层的代码。

  • 第一层是concept,TaskBase。考虑需要的功能后,以虚函数的形式提供对应的接口I。
  • 第二层是model,TaskModel。这是一个类模板,用来存放用户提供的类T,T应当语法上满足接口I;重写concept的虚函数,在虚函数中调用T对应的函数。
  • 第三层是wrapper,对应MyTask。存放一个concept指针p指向model对象m;拥有一个模板构造函数,以适应任意的用户提供类型;以非虚函数的形式提供接口I,通过p调用m。

从技术上来说,这种类型擦除的技巧可算是运行时多态的一种另类实现。它避免了一个类通过继承这种带来类间强耦合关系的方式,去满足某个运行时多态使用(polymorphic use)的需求。

测试代码表明,用户可以把任意的满足void()签名接口的函数对象作为任务,但不需要手动继承任何的代码或编写虚函数。实现任务类的运行时多态的代码被限制在库的范围内,不会以继承的方式侵入用户的代码或其他的库。

这里还有另一个示例。

首先,定义图形的概念接口ShapeConcept,接口类中定义了接口函数Draw。然后,通过模板ShapeModel具体实现了ShapeConcept的概念。最后,定义Shape类来封装ShapeModel。

这样,定义了Draw接口的正方形Square或者是通过特化实现了Draw的圆形Circle,都可以统一到Shape下,而不需要继承它。

#include <iostream>
#include <memory>
#include <utility>
#include <vector>

// 图形的概念接口
struct ShapeConcept
{
    virtual void Draw() const = 0;
};

// 图形概念的具体实现
template <typename T>
struct ShapeModel : ShapeConcept
{
    ShapeModel(T &&val) : shape_{std::forward<T>(val)} {}
    void Draw() const override
    {
        shape_.Draw(); // 这里假设具体图形有Draw()成员函数。如果没有,需要特化该模板。
    }

private:
    // 这里直接存储具体图形的值
    T shape_;
};

// 父类
class Shape
{
public:
    template <typename T>
    Shape(T &&val) : pimpl_{new ShapeModel<T>(std::forward<T>(val))} {}
    inline void Draw() const
    {
        pimpl_->Draw();
    }

private:
    std::unique_ptr<ShapeConcept> pimpl_;
};

//---------------------正方形-------------------------
class Square
{
public:
    explicit Square(float side) : side_(side) {}
    // 正方形的绘图函数是一个成员函数
    void Draw() const
    {
        std::cout << "Draw square of side: " << side_ << std::endl;
    }

private:
    float side_;
};

//---------------------圆-----------------------------
class Circle
{
public:
    explicit Circle(float radius) : radius_(radius) {}
    float GetRadius() const { return radius_; }

private:
    float radius_;
};
// 圆的绘图是一个全局函数
void DrawCircle(const Circle &circle)
{
    std::cout << "Draw circle of radius: " << circle.GetRadius() << std::endl;
}
// 因为圆的绘图函数是一个全局函数,所以需要特化
template <>
struct ShapeModel<Circle> : ShapeConcept
{
    ShapeModel(Circle &&val) : shape_(std::move(val)) {}
    void Draw() const override
    {
        DrawCircle(shape_);
    }

private:
    Circle shape_;
};

int main()
{
    std::vector<Shape> shapes;
    shapes.push_back(Circle(1.0));
    shapes.push_back(Square(2.0));
    for (const auto &shape : shapes)
    {
        shape.Draw();
    }
    return 0;
}

到此这篇关于深入了解C++中基于模板的类型擦除的文章就介绍到这了,更多相关C++类型擦除内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • visual studio 2019工具里添加开发中命令提示符的方法

    visual studio 2019工具里添加开发中命令提示符的方法

    这篇文章主要介绍了visual studio 2019工具里添加开发中命令提示符的方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-03-03
  • 养成良好的C++编程习惯之内存管理的应用详解

    养成良好的C++编程习惯之内存管理的应用详解

    "养成良好的编程习惯"其实是相当综合的一个命题,可以从多个角度、维度和层次进行论述和评判,如代码的风格、效率和可读性;模块设计的灵活性、可扩展性和耦合度等等,要试图把所有方面都阐述清楚必须花很多的精力,而且也不一定能阐述得全面
    2013-05-05
  • C++中类的三种访问权限解析:private、public与protect

    C++中类的三种访问权限解析:private、public与protect

    这篇文章主要介绍了C++中类的三种访问权限解析:private、public与protect,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-11-11
  • C++中的std::async()详解

    C++中的std::async()详解

    这篇文章主要给大家介绍了关于C++中std::async()的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-11-11
  • 一个快速排序算法代码分享

    一个快速排序算法代码分享

    一个快速排序算法代码一个快速排序算法代码,代码内有注释,大家参考使用吧
    2014-01-01
  • 详解C++ sizeof(下)

    详解C++ sizeof(下)

    这篇文章主要介绍了C++ sizeof的相关资料,帮助大家更好的理解和学习c++,感兴趣的朋友可以了解下
    2020-08-08
  • C语言大厂面试技巧及strcpy()函数示例详解

    C语言大厂面试技巧及strcpy()函数示例详解

    这篇文章主要为大家介绍了C语言面试技巧,以strcpy()函数为示例进行分析详解,有需要冲刺大厂的朋友们可以借鉴参考下,希望能够有所帮助
    2021-11-11
  • C语言运算符的重载详解

    C语言运算符的重载详解

    这篇文章主要为大家详细介绍C语言运算符的重载,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下,希望能够给你带来帮助
    2022-02-02
  • C++实现数据结构的顺序表详解

    C++实现数据结构的顺序表详解

    这篇文章主要为大家详细介绍了C++实现动态顺序表,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-11-11
  • C++函数模板学习示例教程指南

    C++函数模板学习示例教程指南

    这篇文章主要为大家介绍了C++函数模板学习示例教程指南,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-04-04

最新评论