想要实现 C++ 管理一段内存块

2018-12-04 12:58:33 +08:00
 yuikns

如下是一个简单的 c++ class。目标是作为一个容器管理一段连续的内存,并且可以简单别太浪费,不要内存泄漏地被 copy/move。

水平太菜自觉考虑难以周全,所以想要求 v 友们指教下代码还有哪些 bug。

无论是本身的指点,还是更好的实现思路的指教都非常感谢。


typedef uint8_t byte;

class Bytes {
 public:
  Bytes() noexcept : data_(nullptr), size_(0) {}

  // 给定 string, 读取数据后保存
  explicit Bytes(const string& data) noexcept
      : Bytes(reinterpret_cast<const byte*>(data.data()), data.size()) {}

  
  explicit Bytes(const byte* bytes, size_t size) noexcept {
    assign(bytes, size);
  }

  Bytes(const Bytes& rhs) noexcept : data_(rhs.data_), size_(rhs.size_) {}

  Bytes(const Bytes&& rhs) noexcept
      : data_(std::move(rhs.data_)), size_(rhs.size_) {}

  virtual ~Bytes() {}

  const size_t size() const noexcept { return size_; }

  const std::shared_ptr<byte> data() const noexcept { return data_; }

  // return is empty
  const bool Empty() const noexcept {
    return data_.get() == nullptr || size() == 0;
  }

  string String() const noexcept {
    return data_.get() == nullptr
               ? string()
               : string(reinterpret_cast<const char*>(data_.get()), size());
  }

  string HexString() const noexcept {
    size_t n = size_;
    char* cout = new char[n * 2];
    const char* rune = "0123456789abcdef";
    for (size_t i = 0; i < n; i++) {
      byte uc = data_.get()[i];
      // little-endian
      cout[2 * i] = rune[uc >> 4];
      cout[2 * i + 1] = rune[uc & 0xf];
    }
    string out(cout, n * 2);
    delete[] cout;
    return out;
  }

  int Compare(const Bytes& that) const noexcept {
    size_t sz = size();
    size_t that_sz = that.size();
    size_t min_sz = (sz < that_sz) ? sz : that_sz;
    int r = memcmp(data().get(), that.data().get(), min_sz);
    return r != 0 ? r : (sz == that_sz ? 0 : (sz < that_sz ? -1 : +1));
  }

  // Return the nth byte in the referenced data.
  // Didn't check: n < size
  byte& operator[](size_t n) noexcept {  //
    return data_.get()[n];
  }

  std::shared_ptr<byte> Resize(size_t size) noexcept {
    std::shared_ptr<byte> new_data(new byte[size],
                                   std::default_delete<byte[]>());
    memset(new_data.get(), 0, size);
    if (this->data_.get() != nullptr) {
      size_t min_sz = (size < this->size_) ? size : this->size_;
      memcpy(static_cast<byte*>(new_data.get()),
             static_cast<const byte*>(this->data_.get()), min_sz);
    }
    this->data_.swap(new_data);
    this->size_ = size;
    return this->data_;
  }

  static Bytes ConcatBytes(const vector<Bytes>& v) noexcept {
    Bytes bytes;
    size_t sz = 0;
    for (auto i : v) {
      sz += i.size();
    }
    bytes.Resize(sz);
    size_t off = 0;
    for (auto i : v) {
      bytes.writeTo(i.data_.get(), off, i.size());
      off += i.size();
    }
    return bytes;
  }

 private:
  std::shared_ptr<byte> data_;
  size_t size_;

  void clear() noexcept {
    data_.reset();
    size_ = 0;
  }

  void writeTo(const byte* bytes, size_t offset, size_t size) noexcept {
    memcpy(data_.get() + offset,  //
           static_cast<const byte*>(bytes), size);
  }

  void assign(const byte* bytes, size_t size) noexcept {
    std::shared_ptr<byte> new_data(new byte[size],
                                   std::default_delete<byte[]>());
    memcpy(static_cast<byte*>(new_data.get()), static_cast<const byte*>(bytes),
           size);
    size_ = size;
    data_.swap(new_data);
  }
};

3317 次点击
所在节点    C
26 条回复
aheadlead
2018-12-04 13:08:13 +08:00
目的只是学习吗?
yuikns
2018-12-04 13:13:53 +08:00
@aheadlead ?

和工作有关么?没有,主力是 scala, python, 为了快速撸个 api 什么还写写 go。可能会是别的自己个人的玩具项目的起点?想
shylockhg
2018-12-04 13:18:55 +08:00
array<uint8_t>
nifury
2018-12-04 13:19:18 +08:00
只是感觉拷贝构造……还是指向同一块内存呀。 如果本意如此的就没问题,但一般不都是新开一块内存么
yuikns
2018-12-04 13:22:59 +08:00
@nifury
就是想默认为指向同一块内存。除非显式拷贝。
yuikns
2018-12-04 13:32:43 +08:00
@shylockhg 对。其实就是想要的就是 shared_ptr<array<uint8_t>> 外加一点别的方法。之前也读了 gcc 版本 array 实现。因为想再加另外几个方法,然后发现期间和 array<uint8_t> 底层交互了多次。想着是不是能直接维护一个底层数组。
zmj1316
2018-12-04 14:27:06 +08:00
和 LS 差不多的问题,拷贝出来指向同一块内存,但是 resize 以后就不是同一块了,换成我用起来会觉得不太直观。
如果真有这种浅拷贝的需求,要不还是分成两个 class ?
ipwx
2018-12-04 14:32:19 +08:00
我觉得在这个语义层级上包装 shared_ptr 是伪需求。
yuikns
2018-12-04 14:37:19 +08:00
@zmj1316 我比较水了啊。因为比较习惯于 scala 那种模式。其实我想要某种容器,开始的时候初始化一下,然后在使用过过程中可能被 cache 一下,或者直接在不需要的时候自动回收。

> 如果真有这种浅拷贝的需求,要不还是分成两个 class ?

其实我也想,要不要做个 Builder + immutable block 会更加合理呢
aa514758835
2018-12-04 14:44:13 +08:00
感觉有点像自己写个动态数组
wevsty
2018-12-04 14:55:59 +08:00
其实需求就是需要一个带引用计数的可变容器,CPP 里面容器直接设计成可变的就行。
如果是不对引用资源进行修改的成员函数直接写成 const 成员函数就行,对于 resize 等等改变容器的成员写为非 const 成员函数或者带有多个重载。
这样容器强调数据不可改变的时候只需要声明时加 const 就可以了,如果不加也可以保证只有在必要时自动对资源进行深度拷贝

以下是建议:
1、建议考虑做成模板。
2、提前考虑线程安全问题,因为涉及引用计数就一定会涉及线程安全的问题。
yuikns
2018-12-04 15:06:09 +08:00
@wevsty 哦!感谢提示!我纠结了好久,我承认从语义上我也觉得太怪了。

模板那个其实有考虑,不过没啥好讨论的,本想此处简单化一点。

关于线程安全。我想象中单线程写多线程读应该是安全的吧.... 我猜?
zmj1316
2018-12-04 15:11:17 +08:00
@yuikns 不一定的

`
this->data_.swap(new_data);
this->size_ = size;
`

如果是在这两句中间进行读取就会出现数据和 size 不匹配,需要做读写锁的处理
wutiantong
2018-12-04 15:30:34 +08:00
C++里面做数据结构封装时的最佳实践就是值语义,而你这段代码就是一个典型的违反值语义的示范。
那么这会带来什么问题呢?请看下面的例子:

Bytes s1("Hello");
auto s2 = s1;
s2[1] = 'a';

对 s2 的改动会意外的传递给 s1,在现代 C++中这段代码表现出来的行为是完全反直觉的。

同样在内部涉及到大量的动态内存分配操作,而 STL 中的各种容器( vector, set, map )无一例外的遵循着值语义。

C++之所以提倡值语义而不必担心引入额外的性能开销,是因为 C++提供了引用,指针,移动这一系列利器。

也就是说当你实现了一个值语义的 Bytes 后,你就可以免费享受:
1. Bytes &
2. Bytes *
3. Bytes &&
4. std::shared_ptr<Bytes>, std::unique_ptr<Bytes>

而像现在这样一个不遵循值语义的 Bytes 只会引入无尽的混乱。
wutiantong
2018-12-04 15:33:14 +08:00
正如 @ipwx #8 所说,针对 shared_ptr 的这种封装可以认为是一种伪需求。
yuikns
2018-12-04 15:56:01 +08:00
@wutiantong 感谢指点。操作符那个是我晚饭把清水当白酒脑残了。其实应该提供是 at 或者没有引用的 byte。

比如这儿: https://github.com/abseil/abseil-cpp/blob/master/absl/strings/string_view.h#L499

此处是用的裸指针。我想象中,想要有一个这样的,生成后就 immutable 的,可以自己消亡的,有 concat 的,内部如它这样管理的某种容器。

建议就是直接用 std::shared_ptr<string_view> ?
wevsty
2018-12-04 16:07:14 +08:00
@yuikns
不用模板在遇到某些特定类型的情况下会更麻烦一些,因为 STL 容器基本都有固定的成员函数这样可以简化很多。
比如需要 const string& data 的构造函数在遇到 const std::vector<char>& data 这种东西的时候就会容易蛋疼了。

单线程写多线程读的情况下线程安全问题,std::shared_ptr 保证了原子性所以常见的坑基本避免了,剩下的得看你的需求和容器怎么设计。比如:是否希望某个线程的修改同步到其他线程。
wevsty
2018-12-04 16:19:47 +08:00
@zmj1316

如果是多线程共享同一个对象,那么在这里确实会有问题。

不过补充一下:
如果不在多线程之间共享同一个对象的话,那么这样就没有问题,执行 resize 的当前线程不可能在执行这两句的时候再去读取所以当前线程不会出错。又因为 std::shared_ptr 内部实现了引用计数,其他线程持有的这份资源不会被释放也不会被修改,并且因为其他的线程并不是同一个对象,引用和长度都不会改变,所以其他线程也不会出问题。
wutiantong
2018-12-04 16:43:46 +08:00
@yuikns

std::shared_ptr<string_view> 这个用法就很诡异了,几乎不存在。。。
因为 string_view 这个类不负责底层指针的生命周期管理,给它外面套一层 shared_ptr 并不会改变什么。。。

你说的这个需求,我以前刚好粗略实现过一个很类似的,待会可以发一下。

但说实在的,这种东西真的是用途不大,毕竟 immutable 太死板了。
目前来看最佳实践应该是 string + string_view 这种形式。
yuikns
2018-12-04 17:13:31 +08:00
@wutiantong 对。我想象中需要一个管理 string_view 生命周期的那么一个容器,功能就想要一次写入剩下的就是散到什么线程里面若干只读。开始本来想用 vector 来着,一看 append 是长度 *2, 又想用 array, 发现好像用来用去就只要底层数据,那为何不直接裸指针放一起呢?当时是这么想的。

我比较蠢啦,就是感觉 immutable 配合 map-reduce 这种并行模式特别简单好用,不过在这些容器实现细节上感觉自己太弱了,所以举个栗子,想求教下这类处理各种细节问题。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/514171

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX