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

一、问题简介

       在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;
    }
}

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/870977.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【洛谷训练记录】【LGR-213-Div.4】洛谷入门赛 #31

训练情况赛后反思 模拟题差点红温,差一道字符串模拟题AK A题 问一个数 \(a\) 加多少后的个位数变成 \(b\),取出 \(a\) 的个位数,再用 \(b\) 去减,如果小于零答案再加十。 #include <bits/stdc++.h> // #define int long long #define endl \nusing namespace std;voi…

WSL2配置代理

1、关闭梯子 2、设置为mirrored模式该设置会生成 C:/Users//.wslconfig【可选】修改.wslconfig文件内容如下 [experimental] autoMemoryReclaim=gradual networkingMode=mirrored dnsTunneling=true firewall=true autoProxy=true3、打开cmd并执行wsl --shutdown 4、重新启动…

【MySQL架构】图解

你是一个程序员,你做了一个网站应用,站点里的用户数据,需要存到某个地方,方便随时读写。 很容易想到可以将数据存到文件里。 但如果数据量很大,想从大量文件数据中查找某部分数据,并更新,是一件很痛苦的事情。 那么问题就来了,有办法可以解决这个问题吗? 好办,没有什…

2025 郑州一测 T18: 双变量问题探讨

2025 高考加把劲 /qtContent已知函数 \(f(x) = \log_a x(a>0, a\neq 1)\), \(y = f(x)\) 关于 \(y=x\) 对称的函数记为 \(g(x)\). (I) 若 \(a>1\), 方程 \(f(x)-g(x)=0\) 有且仅有一个实数解, 求 \(a\) 的值. (II) 讨论方程 \(g(x) = x_a = 0\) 在 \((0, +\infty)\) 上实…

笑死

哈哈哈哈哈笑死这个中国银行的标志出现的恰到好处,我还以为是什么标识

C#数据结构与算法入门实战指南

前言 在编程领域,数据结构与算法是构建高效、可靠和可扩展软件系统的基石。它们对于提升程序性能、优化资源利用以及解决复杂问题具有至关重要的作用。今天大姚分享一些非常不错的C#数据结构与算法实战教程,希望可以帮助到有需要的小伙伴。 C#经典十大排序算法 主要讲解C#经典…

29. 数据库操作

一、SQL与数据库数据库 (database)是统一管理的、有组织的、可共享的大量数据的集合。数据库将数据存储在一个或多个表格中,管理这个数据库的软件称为 数据库管理系统(database management system, DBMS)。数据库不是针对具体的应用程序,而是立足于数据本身的管理,它将所…

用Mermaid画图

1、用Mermaid画图 mermaid.md TyporaPortable.rar mermaid.zip 目录1 Mermaid是什么1.1 概述1.2 网址官网地址:Github地址:图形图形几种图形名字节点与无名字节点设置样式:style, classDef, class, :::线条图形连线(--)及注释(%%)线条样式实线与虚线箭头实线与粗实线及箭头延…

【RabbitMQ】图解

你是一个程序员,假设你维护了两个服务 A 和 B。 A 服务负责转发用户请求到 B 服务,B 服务是个算法服务,GPU 资源有限,当请求量大到 B 服务处理不过来的时候,希望能优先处理会员用户的请求。 那么问题就来了,如果普通用户和会员用户同时发起请求,怎样才能做到会员优先呢?…

THREE.js学习笔记8——Textures

这个小节主要学习纹理,Texture 纹理是覆盖几何形状表面的图像,不同类型的纹理具有多种不同的效果。 这些纹理(尤其是金属性和粗糙度)遵循PBR原则基于物理的渲染 许多技术往往遵循现实生活中的方向以获得现实的结果 成为现实渲染的标准 许多软件、引擎和库都在使用它如何加载…

快速傅里叶变换总结

基本概念 对于求和式 \(\sum a_ix^i\),如果是有限项相加,称为多项式,记作 \[f(x)=\sum_{i=0}^n a_ix^i。 \]其中最高次项的次数为 \(n\),为 \(n\) 次多项式。 用 \(n+1\) 个点可以唯一地确定一个 \(n\) 次多项式,这一过程可以参考 拉格朗日插值。 引入 给定多项式 \(f(x),…

寒假学习1

老年人评估系统 初步整理web端思路先写了第一张信息表并搭建基本框架并编写了老年人信息添加功能