V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
yuikns
V2EX  ›  C

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

  •  
  •   yuikns · 2018-12-04 12:58:33 +08:00 · 3425 次点击
    这是一个创建于 2211 天前的主题,其中的信息可能已经有所发展或是发生改变。

    如下是一个简单的 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);
      }
    };
    
    
    26 条回复    2018-12-05 12:17:56 +08:00
    aheadlead
        1
    aheadlead  
       2018-12-04 13:08:13 +08:00
    目的只是学习吗?
    yuikns
        2
    yuikns  
    OP
       2018-12-04 13:13:53 +08:00
    @aheadlead ?

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

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

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

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

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

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

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

    如果是在这两句中间进行读取就会出现数据和 size 不匹配,需要做读写锁的处理
    wutiantong
        14
    wutiantong  
       2018-12-04 15:30:34 +08:00   ❤️ 2
    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
        15
    wutiantong  
       2018-12-04 15:33:14 +08:00   ❤️ 1
    正如 @ipwx #8 所说,针对 shared_ptr 的这种封装可以认为是一种伪需求。
    yuikns
        16
    yuikns  
    OP
       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
        17
    wevsty  
       2018-12-04 16:07:14 +08:00   ❤️ 1
    @yuikns
    不用模板在遇到某些特定类型的情况下会更麻烦一些,因为 STL 容器基本都有固定的成员函数这样可以简化很多。
    比如需要 const string& data 的构造函数在遇到 const std::vector<char>& data 这种东西的时候就会容易蛋疼了。

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

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

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

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

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

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

    我比较蠢啦,就是感觉 immutable 配合 map-reduce 这种并行模式特别简单好用,不过在这些容器实现细节上感觉自己太弱了,所以举个栗子,想求教下这类处理各种细节问题。
    wutiantong
        21
    wutiantong  
       2018-12-04 17:20:36 +08:00   ❤️ 1
    nmgwddj
        22
    nmgwddj  
       2018-12-04 19:20:17 +08:00
    世界上最痛苦的事情就是在用 C++ 还要自己管理内存!
    exonuclease
        23
    exonuclease  
       2018-12-04 19:42:38 +08:00 via iPhone   ❤️ 1
    拷贝构造函数只是复制指针?这个设计有问题吧。我觉得既然提供了移动构造函数 拷贝构造函数就应该实现成拷贝数据了
    yuikns
        24
    yuikns  
    OP
       2018-12-05 02:15:47 +08:00
    @wutiantong 👍


    @exonuclease 我也觉得语义和约定的不一样。因为想要一个构造完毕后就不再动的结构,因此深拷贝并不需要。发现很多工程以及 stl 里面设计都是直接类似结构的 copy 置为 delete,菜鸟不懂啦,我一般是直接照着这种设计做,并不知其所以然。但有的时候我就想了,为什么一定要自己再写个指针呢?能不能更简单点过去呢?所以才贴了如上例子。
    exonuclease
        25
    exonuclease  
       2018-12-05 09:38:33 +08:00 via iPhone   ❤️ 1
    @yuikns stl 通常要么不允许拷贝 要么就实现成值语义的 我觉得倒是比别的语言里面那种到处引用来引用去的符合直觉 也更安全 别的语言那么搞是为了性能 c++可以移动构造 填了一些性能的坑 你用了 shared_ptr 的话 实现成这样其实也是安全的 就是不符合大家写 c++的习惯
    Mirana
        26
    Mirana  
       2018-12-05 12:17:56 +08:00
    没看懂这是个啥,还以为是 allocator
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5651 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 06:48 · PVG 14:48 · LAX 22:48 · JFK 01:48
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.