首页 > 其他分享 >STL容器封装常见问题分析解决方法总结

STL容器封装常见问题分析解决方法总结

时间:2025-01-17 21:56:00浏览次数:1  
标签:std 容器 常见问题 封装 STL 运算符 分配器 allocator

一、问题简介

       在C++的开发工作中,经常会将STL的标准容器进行一层封装,以满足更高级的需求,如支持外部内存等。在封装容器时,容易出现问题的地方包括容器的元素运算符以及容器的内存分配器,本人在做相关的工作时,将上述两方面所遇到问题的分析解决方法进行了如下总结。

二、问题一:重载运算符时的问题

        对于被封装的STL容器,由于其可能作为自定义数据类型而成为其他容器(如基于RB Tree的容器map和基于Hash Table的容器unordered_set)的元素,所以需要在封装时重载容器的运算符。具体需要重载的运算符包括用于RB Tree中排序的比较运算符"<",以及用于无序容器中判等使用的"=="运算符,如在封装容器类“CVector”中可以进行以下重载:

friend bool operator < (const CVector<T, Allocator> &vec1, const CVector<T, Allocator> &vec2)
{
    return vec1.m_vector < vec2.m_vector; // 重载运算符"<"
}
friend bool operator == (const CVector<T, Allocator> &vec1, const CVector<T, Allocator> &vec2)
{
    return vec1.m_vector == vec2.m_vector; // 重载运算符"=="
}

        对于C++的类,重载运算符的函数可以声明为成员函数,也可以声明为友元函数,如对于一个“person”类:

class person {
public:
    bool operator<(const Person &arg); // 重载运算符的函数作为成员函数
private:
    int a;
};
bool person::operator<(const Person &arg)
{
    if((this->a) < arg.a) {}
}
// or---------------------------------------------------------------
class person {
public:
    friend bool operator<(const Person& arg1, const Person& arg2); // 重载运算符的函数作为友元函数(友元函数不属于成员函数)
private:
    int a;
};
bool operator<(const Person& arg1, const Person& arg2) // 实现时不需要类名限定,不属于任何类
{
    if(arg1.a < arg2.a) {}
}

但是,在封装容器的内部,“<“和”= =“运算符只能声明为友元函数,因为封装容器时使用了类模板,所以封装的容器是可以被继承的,当继承下来的子类容器的对象被装载进别的容器时,子类对象和父类对象一样可以执行”<“或”= =“操作,这个过程其实是发生了隐式类型转换。友元函数没有this指针,所需操作数都必须在参数表显式声明,很容易实现类型的隐式转换,所以使用友元函数就满足了第一个参数需要隐式转换的需求,使用成员函数重载运算符则不满足要求。

       然而,容器内部的另一些运算符重载时则只能声明为成员函数,如赋值运算符”=”、函数调用运算符“()“、下标运算符”[]”和通过指针访问的类成员运算符”->,如下所示。在重载上述运算符时,它们左侧的操作数均为重载了运算符类的对象,如果把这些运算符重载为友元函数,这样的话一些非左值(如常量)会被编译器隐式转换为一个临时对象,这样非左值就会出现在运算符的左边,破坏了运算符的语义,如2=C;另外,编译器也会提供一个默认的赋值运算符,如果自己定义为友元函数,函数的参数列表不一样,不会发生重载。

CVector &operator=(const CVector<T, Allocator> &vec)  // 重载运算符"="
{
    m_vector = vec.m_vector;
    return *this;
}
T &operator[](const int &n)  // 重载运算符"[]"
{
    return m_vector[n];
}

 因此,在封装容器的内部,重载运算符可以总结为两种情况:
        a.运算符左侧操作数能够发生隐式类型转换,借助友元函数重载;
        b.运算符左侧操作数不能发生隐式类型转换,使用成员函数重载。

三、问题二:分配器构造函数的问题

        封装容器的内存分配器问题容易出现在封装基于Hash Table的容器时,通常会遇到如下错误:

error: no matching function for call to ‘myAlloc::CAllocator<std::__detail::_Hash_node_base*, (_MEM_TYPE_E)0u, 0u>::CAllocator(std::_Hashtable<int, int, myAlloc::CAllocator<int, (_MEM_TYPE_E)0u, 0u>, std::__detail::_Identity, std::equal_to<int>, std::hash<int>, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<false, true, true> >::_Node_allocator_type&)’_Bucket_allocator_type __alloc(_M_node_allocator());
上述错误指没有匹配的函数调用自定义分配器,可能的原因包括以下两方面。

       a.封装容器底层的_Hashtable使用自定义内存分配器导致的错误,因此先验证该猜测:

验证步骤一:_Hashtable使用STL自带的内存分配器。

using _Key   = int;
using _Value = int;
using _Tp    = std::pair<const _Key, _Value>;
using _Hash  = std::hash<_Key>;
using _Pred  = std::equal_to<_Value>;
using _Alloc = std::allocator<_Tp>;  // 使用STL自带的内存分配器
using _Tr    = std::__umap_traits<std::__cache_default<_Key, _Hash>::value>;
std::_Hashtable<_Key, _Tp, _Alloc, std::__detail::_Select1st, _Pred, _Hash, std::__detail::_Mod_range_hashing,std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, _Tr> rr();

 步骤一结果:_Hashtable正常,未报错,排除_Hashtable本身的问题。

验证步骤二:_Hashtable使用自定义的分配器。

using _Key   = int;
using _Value = int;
using _Tp    = std::pair<const _Key, _Value>;
using _Hash  = std::hash<_Key>;
using _Pred  = std::equal_to<_Value>;
using _Alloc = myAlloc::CAllocator<_Tp, MEM_MALLOC_TYPE, nullptr>;  // 使用自定义的内存分配器
using _Tr    = std::__umap_traits<std::__cache_default<_Key, _Hash>::value>;
std::_Hashtable<_Key, _Tp, _Alloc, std::__detail::_Select1st, _Pred, _Hash, std::__detail::_Mod_range_hashing,std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, _Tr> rr();

 步骤二结果:_Hashtable正常,未报错,排除_Hashtable使用自定义分配器的问题。

       b.封装容器本身使用自定义分配器导致的错误,验证该猜测的方法如下: 

验证步骤一:封装容器“CUnordered_set”使用STL自带的内存分配器。

// CUnordered_set<int, myAlloc::CAllocator<int, MEM_MALLOC_TYPE, nullptr> > myUnordered_set;
CUnordered_set<int, std::allocator<int> > myUnordered_set;

 步骤一结果:封装容器正常,未报错,排除封装内容中除内存分配器以外的问题。

验证步骤二:封装容器“CUnordered_set”使用自定义的内存分配器。

CUnordered_set<int, myAlloc::CAllocator<int, MEM_MALLOC_TYPE, nullptr> > myUnordered_set;
// CUnordered_set<int, std::allocator<int> > myUnordered_set;

 步骤二结果:封装容器报错。

        因此,错误原因在于自定义内存分配器的问题。通过与STL自带的分配器的比较发现,自定义分配器缺少一个默认的构造函数、一个默认的拷贝构造函数和一个泛化的拷贝构造函数。

constexpr CAllocator() noexcept {}
constexpr CAllocator(const CAllocator&) noexcept = default;
template <typename _Other> constexpr CAllocator(const CAllocator<_Other>&) noexcept {}

 其中,泛型构造函数的存在主要为了解决STL库的设计问题。容器模板的第二个参数一般是分配器allocator,它是有类型参数T的,在容器的实现中,STL库最初假设被创建出来的对象也是T类型的,但是对于一些容器来说,这个假设是不成立的,所以STL库使用偏特化来提供不同的构造函数,帮助allocalor从一种类型伪装成另一种类型的allocator,从而可以作为容器的模版参数,然后在内部转换成原本的allocator,这就是泛型构造函数的作用。在模板类型转换时,STL中提供了rebind接口来实现,它的作用就是对于类型T的分配器allocator<T>,可以根据相同的策略得到另外一个类型U的分配器allocator<U>,并且类型T和类型U在逻辑上是相关的,比如在_Hashtable中,数据的类型和结点hash_node的类型就是有联系的,它们对的内存分配策略是一样的。

       上面的过程属于重绑定机制,模板构造函数和rebind结构,其实是分别解决了两个不同的问题,一个针对实例,一个针对类型:a.模板构造函数所解决的问题是,如果容器有了allocator<T1>的实例,如何构造出allocator<T2>的实例;b.rebind所解决的问题是,如果一个容器得到了allocator<T1>类型,在内部如何能得到allocator<T2>类型。

       分配器的构造函数内部实际上没有做任何事,不需要初始化任何成员变量,所以任意两个malloc_allocator都是可互换的。如果a1和a2的类型都是malloc_allocator<int>,则可以自由地通过a1来allocate()内存,然后通过a2来deallocate()它,于是在自定义的分配器内部还需要定义比较操作“==”和“!=”以表明所有的malloc_allocator对象是否是等价的,如下所示。

template <typename _Ty, typename _Other> 
inline bool operator==(const CAllocator<_Ty>&, const CAllocator<_Other>&) noexcept {
    if(typeid(_Ty) == typeid(_Other)) { 
        return true; 
    }
    else { 
        return false; 
    }
}

template <typename _Ty, typename _Other> 
inline bool operator!=(const CAllocator<_Ty>&, const CAllocator<_Other>&) noexcept  {
    if(typeid(_Ty) != typeid(_Other)) {
        return true;
    }
    else {
        return false;
    }
}

 

标签:std,容器,常见问题,封装,STL,运算符,分配器,allocator
From: https://www.cnblogs.com/kongzimengzixiaozhuzi/p/18677718

相关文章

  • Java初学者笔记-01、封装继承多态
    封装:封装是指隐藏对象的属性和实现细节,仅对外提供公共访问方式。通过封装,可以将类的信息隐藏在类内部,只暴露对外的接口(如setter和getter方法),从而提高代码的安全性和可维护性。继承:继承是从已有的类中派生出新的类的过程。新的类(子类)能够吸收已有类(父类)的数据属性和行为,并且可以......
  • ZooKeeper 常见问题与核心机制解析
    Zookeeper集群本身不直接支持动态添加机器。在Zookeeper中,集群的配置是在启动时静态定义的,并且集群中的每个成员都需要知道其他所有成员。当你想要增加一个新的Zookeeper服务器到现有的集群中时,你需要更新所有现有服务器的配置文件(通常是zoo.cfg文件),以包含新的服务器信息。......
  • unordered_map-STL容器
    时间复杂度和空间复杂度......
  • 使用python+pytest+requests完成自动化接口测试(包括html报告的生成和日志记录以及层级
    一、API的选择我们进行接口测试需要API文档和系统,我们选择JSONPlaceholder免费API,因为它是一个非常适合进行接口测试、API测试和学习的工具。它免费、易于使用、无需认证,能够快速帮助开发者模拟常见的接口操作(增、删、改、查)。尤其对于我你们学习接口测试的初学开发者来说,它......
  • GaussDB 客户端工具--gsql常见问题处理
    GaussDB-常见问题处理连接性能问题开启log_hostname,但是配置错误的DNS导致的连接性能问题。连接数据库,通过“showlog_hostname”语句,检查数据库中是否开启log_hostname参数。如果开启了相关参数,那么数据库内核通过DNS反查客户端所在机器的主机名。如果数据库配置了不正确......
  • 分别封装精确运算的加减乘除四个方法
    在前端开发中,进行精确的加减乘除运算通常是因为JavaScript的浮点数运算存在精度问题。为了解决这个问题,可以使用一些库,如decimal.js或big.js,或者手动实现这些方法。以下是一个简单的示例,使用JavaScript手动封装精确的加减乘除四个方法:/***精确加法*@param{number}num1......
  • 封装
    封装1.封装思想封装概述是面向对象三大特征之一(封装,继承,多态)对象代表什么,就得封装对应的数据,并提供数据对应的行为封装代码实现将类的某些信息隐藏在类内部,不允许外部程序直接访问,而是通过该类提供的方法来实现对隐藏信息的操作和访问成员变量private,提供对应的getXxx(......
  • element中的条件字段页面封装
    笔记:页面搜索栏封装: <tableQueryFormref="queryFormRef":form-select="QUERY_FORM_SELECT":model="queryForm"><el-form-item:label="$swpT('page.swModulePlan.isSelf')"prop="orderCode&......
  • 封装按钮信息与按钮数量动态显示与提示信息并进行触发按钮组件
    标题:封装弹框并点击动态按钮组件进行触发功能:封装按钮信息与按钮数量动态显示组件页面:按钮信息与按钮数量动态显示,提示信息不固定封装组件页面: <template><el-dialogv-model="dialogVisible"class="stepCustom-dialogcustom-dialog-center"destroy-on-closeheight=......
  • 图表封装组件
    图表封装: 图表封装使用:<template><!--软件模块复用率图表--><chart-and-tableref="chartAndTableRef":chart-desc="chartDesc":chart-loading="chartLoading":columns="columns":list="list":......