C++实现Go中的Slice

引言

在学习Go语言的过程中,感觉Go中的切片Slice挺好用的,就看了下实现的原理并自己用C++实现了一个类似的.

本文分为三部分,第一部分介绍Go中Slice的使用及原理,第二部分是笔者如何使用C++实现类似的功能

Go Slice 的使用及原理

使用

Go Slice的使用与原理在文章: Go Slices: usage and internals 中有更详细的介绍, 在此仅作简单的说明, 想要了解更多的内容可以查看该文章.

Slice 与数组类似,都可以存放多个相同类型的元素,不同的地方在于数组的长度是固定的,而Slice的长度是可变的. 在Go中可以通过调用make来创建一个Slice:

// make函数的声明
func make([]T, len, cap) []T
// 创建一个长度和容量都为5的切片,
s1 := make([]byte, 5, 5)
s2 := make([]byte, 5)

s1 是一个长度和容量均为5的切片, s2只指定了长度,没有指定容量,没有指定时使用长度作为其容量
要获取其长度和容量可以使用lencap函数:

len(s1) == 5
cap(s1) == 5

原理

一个Slice是数组的一个片段, 其中包含了指定数组的指针,片段的长度和容量
一个Slice是数组的一个片段, 其中包含了指定数组的指针,片段的长度和容量
如Slices := s1[2:4]表示为:
s := s1[2:4]
要增加slice的容量必须创建一个新的,更大的slice,并将旧的内容从原slice拷贝到新slice中. Go 提供了append函数来完成这件事

func append(s []T, x ...T) []T

append函数将元素x添加到切片的末尾,当切片增长到超过其容量时会自动扩大其容量.

C++中Slice的实现

分析

要实现Slice的容量增加,最直观的方式就是使用C++标准库中的vector来实现, 再增加一个表示该切片的起始下标,和一个表示数据长度的值就可以实现类似的功能.
由于Slice与容器类似,因此使用模板类实现.由于语法上的不同,部分相同的功能在C++中的调用方式与Go中不同.

 	/*********************************************************************
     * slice 模仿Go中的切片实现的切片
     * 创建: 可以创建空的切片,也可以使用makeSlice()函数创建切片并指定切片长度
     * slice目标在于尽可能提供与Go中的slice相同的功能,由于C++本身语法的限制
     * 部分功能如[:]无法直接通过重载[]实现,改为重载()实现,
     * 但总体上应该能与Go中的slice表现出一致的行为
     * ******************************************************************/
    template <typename T>
    class slice
    {
    private:
    	std::shared_ptr<std::vector<T>> m_data;
        size_t m_beginIndex;
        size_t m_len;
    }

每个slice包含一个指向存储实际数据的std::vector<T>的共享指针std::shared_ptr, 可能有多个slice指向同一份数据,因此使用shared_ptr来管理; 数据需要连续存放,且空间可增长,故使用std::vector
m_beginIndex表示切片的数据在底层数据中的起始位置
m_len表示切片中数据的长度

构造函数

最简单的构造函数就是创建一个空的slice:

explicit slice() 
	: m_data(nullptr), m_beginIndex(0), m_len(0)
{}

创建空slice时使用空指针初始化m_data, 再有数据时在分配空间. m_beginIndexm_len均初始化为0,

处理直接创建一个空的切片,Go提供了根据数组创建切片的功能, 并直接使用数组的作为底层数据.但是C++中, 若创建的数组被释放,使用了该数组的slice将访问已释放了的数据. 为此, C++中根据数组创建slice需要复制数组中的数据.

		/****************************************************************
         * 使用数组初始化切片, 必须指定起始位置及长度
         * 调用者需要保证数据的可用
         * **************************************************************/
        explicit slice(T data[], size_t beginIndex, size_t len)
            : m_beginIndex(0), m_len(len)
        {
            if (len == 0)
            {
                return;
            }
            m_data = std::make_shared<std::vector<T>>();
            m_data->reserve(len);
            m_data->insert(m_data->end(), std::begin(data), std::end(data));
        }

参数与Go中有所差别,其表现行为也有所差别:
在Go中,若时修改了数组,则相关的切片的数据也会发生变化, 如:

func main() {
	primes := [6]int{2, 3, 5, 7, 11, 13}

	var s []int = primes[1:4]
	fmt.Println(s)
	primes[1] = 0
	fmt.Println(s)
}

输出结果为:

[3 5 7]
[0 5 7]

但是此时C++的表现行为则不同:

int main(int, char**) {
    int data[4] = {1, 2, 3, 4};
    slice<int> s(data, 0, 4);
    cout << s << endl;
    data[0] = 0;
    cout << s << endl;
    return 0;
}

输出结果为:

[1, 2, 3, 4]
[1, 2, 3, 4]

为此,提供另一个构造函数:

		slice(std::initializer_list<T> list)
            : m_data(std::make_shared<std::vector<T>>(list)),
              m_beginIndex(0), m_len(list.size())
        {
        }

使用:

int main(int, char**) {
    slice<int> s{1, 2, 3, 4};
    cout << s << endl;
    return 0;
}

因此,在C++中,需要一开始就从切片开始. 如:

int main(int, char**) {
    slice<string> s{"Hello", "World"};
    cout << s << endl;
    auto s2 = s;
    cout << s2 << endl;
    s[0] = "";
    cout << s2 << endl;
    return 0;
}

输出:

[Hello, World]
[Hello, World]
[, World]

拷贝构造函数

在上面中使用了拷贝构造函数:

		slice(const slice<T>& otherslice)
            : m_data(otherslice.m_data), m_beginIndex(otherslice.m_beginIndex),
              m_len(otherslice.m_len)
        {
        }

重载<<运算符

template <typename T>
class slice{
 		std::string toString() const
        {
            if (isEmpty())
            {
                return "[]";
            }
            std::stringstream ss;
            ss << "[";
            for (size_t i = 0; i < m_len - 1; i++)
            {
                ss << (*m_data)[i + m_beginIndex] << ", ";
            }
            ss << (*m_data)[m_beginIndex + m_len - 1];
            ss << "]";
            return ss.str();
        }
    };
        template<typename T>
	    std::ostream& operator<<(std::ostream& out, const slice<T>& slice)
	    {
	        out << slice.toString();
	        return out;
	    }

append

Go中的slice的增长通过append实现,C++中类似功能的实现为:

template<typename T>
class slice{
		slice<T> append(std::initializer_list<T> list)
        {
            if(m_beginIndex == 0 && m_len == m_data->size())
            {
                // 位于vector末尾,不需要拷贝数据,直接再末尾添加数据
                m_data->reserve(m_data->size() + list.size());
                m_data->insert(m_data->end(), list.begin(), list.end());
                m_len = m_data->size();
            }
            else
            {
                auto tempData = std::make_shared<std::vector<T>>();
                tempData->reserve(m_data->size() + list.size());
                tempData->insert(tempData->end(), m_data->begin(), m_data->end());
                tempData->insert(tempData->end(), list.begin(), list.end());
            }
            
            return slice<T>(*this);
        }
};
	/*****************************************************************
     * 全局的append函数,与Go的append功能一致
     * 由于使用变长参数时,若不添加一个值表示有多少个值则无法判断参数到哪里
     * 结束,故改用std::initializer_list<T>,在调用时需要使用{}将值列表
     * 包裹起来。如append(s {3, 2, 1});
     * ***************************************************************/
    template<typename T>
    slice<T> append(const slice<T>& srcSlice, std::initializer_list<T> list)
    {
        slice<T> resSlice(srcSlice);
        resSlice.append(list);
        return resSlice;
    }

总结

由于语言本身的限制, 个人能力有限, 无法在C++中实现功能与Go中的slice完全一致的slice, 但是实现了一个总体功能类似的slice
具体实现还有额外的几个函数,并没有全部放上来.
完整的实现代码: https://github.com/muzhy/furry-toy/blob/main/dataStruct/slice.hpp
以及相关的测试用例: https://github.com/muzhy/furry-toy/blob/main/test/dataStruct/slice_test.cpp
实现并没有经过完整的测试, 欢迎大家指出不足

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值