You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C++写时复制子串/子数组及静态C字符串抽象方案问询

Hey there! Great question—this is exactly the kind of design that makes high-performance C++ text processing feasible without sacrificing safety or flexibility. You’re on the right track with combining Go-style slice semantics, copy-on-write (COW), and reference counting to avoid unnecessary copies for large parsing workloads. Let’s walk through a complete, practical solution that covers all your requirements:

Core Design: Reference-Counted COW String Slice

The goal is to create a class that acts like a Go slice (pointer + offset + length) but adds a reference-counted control block to manage ownership of the underlying buffer. This lets us:

  • Substring without copying data (just adjust the offset/length)
  • Use COW to defer copying until modification is needed
  • Handle both heap-allocated buffers and static C-strings with the same interface

Internal Structure

We’ll split the class into two parts:

  1. Control Block: Stores the raw buffer pointer, total buffer capacity, atomic reference count, and a flag marking if the buffer is static (to avoid freeing read-only memory).
  2. Slice Instance: Holds a pointer to the control block, a start offset into the buffer, and the length of the slice.

Handling Static C-Strings

For static C-strings (like const char* "hello"), we’ll mark their control block as is_static and skip reference counting for them—since static memory is never freed, we don’t need to track ownership. When someone tries to modify a static slice, COW will kick in and copy the data to a writable heap buffer.

Copy-on-Write Logic

Whenever a modification is requested (e.g., changing a character), we first check if the control block has multiple references. If it does, we copy the relevant portion of the buffer to a new heap allocation, create a new control block for it, and adjust the slice to point to this new buffer. If the slice is the only owner (or the buffer is static), we either modify directly (for unique heap buffers) or copy first (for static buffers).

Key Implementation Details

Here’s a simplified, working skeleton of the class:

Control Block Definition

#include <cstring>
#include <atomic>
#include <stdexcept>

struct BufferControlBlock {
    const char* data;
    size_t capacity;
    std::atomic<size_t> ref_count;
    bool is_static;

    BufferControlBlock(const char* d, size_t cap, bool static_buf)
        : data(d), capacity(cap), ref_count(1), is_static(static_buf) {}
};

Slice Class Implementation

class StringSlice {
private:
    BufferControlBlock* ctrl_ = nullptr;
    size_t start_ = 0;
    size_t length_ = 0;

    // Trigger copy-on-write when modification is needed
    void copy_on_write() {
        if (!ctrl_) return;

        // No need to copy if we're the only owner (and not static)
        if (!ctrl_->is_static && ctrl_->ref_count == 1) {
            return;
        }

        // Allocate new buffer (only copy the portion our slice uses)
        char* new_data = new char[length_];
        std::memcpy(new_data, ctrl_->data + start_, length_);

        // Create new control block for the copied data
        BufferControlBlock* new_ctrl = new BufferControlBlock(new_data, length_, false);

        // Release reference to old control block
        if (!ctrl_->is_static && --ctrl_->ref_count == 0) {
            delete[] ctrl_->data;
            delete ctrl_;
        }

        // Update our slice to point to the new buffer
        ctrl_ = new_ctrl;
        start_ = 0; // New buffer starts at offset 0 for our slice
    }

public:
    // Construct from static C-string
    StringSlice(const char* static_str) {
        if (!static_str) throw std::invalid_argument("Null static string");
        size_t cap = std::strlen(static_str);
        ctrl_ = new BufferControlBlock(static_str, cap, true);
        ctrl_->ref_count = 0; // Static buffers don't need ref counting
        length_ = cap;
    }

    // Construct from heap-allocated buffer (takes ownership)
    StringSlice(char* heap_data, size_t cap) {
        if (!heap_data) throw std::invalid_argument("Null heap buffer");
        ctrl_ = new BufferControlBlock(heap_data, cap, false);
        length_ = cap;
    }

    // Create substring WITHOUT copying data
    StringSlice substring(size_t start_offset, size_t sub_len) const {
        if (!ctrl_) throw std::logic_error("Empty slice");
        if (start_offset + sub_len > length_) throw std::out_of_range("Substring out of bounds");

        StringSlice result = *this;
        result.start_ += start_offset;
        result.length_ = sub_len;

        // Increment ref count if not static
        if (!ctrl_->is_static) {
            ++ctrl_->ref_count;
        }

        return result;
    }

    // Modify a character (triggers COW if needed)
    void set_char(size_t pos, char c) {
        if (!ctrl_) throw std::logic_error("Empty slice");
        if (pos >= length_) throw std::out_of_range("Position out of bounds");

        copy_on_write();
        // Cast away const because we've ensured we own a writable buffer
        char* writable_data = const_cast<char*>(ctrl_->data);
        writable_data[start_ + pos] = c;
    }

    // Getters for read-only access
    const char* data() const { return ctrl_ ? (ctrl_->data + start_) : nullptr; }
    size_t length() const { return length_; }
    bool empty() const { return length_ == 0; }

    // Destructor: Clean up control block if no references left
    ~StringSlice() {
        if (!ctrl_) return;
        if (!ctrl_->is_static && --ctrl_->ref_count == 0) {
            delete[] ctrl_->data;
            delete ctrl_;
        }
    }

    // Copy constructor: Share control block, increment ref count
    StringSlice(const StringSlice& other)
        : ctrl_(other.ctrl_), start_(other.start_), length_(other.length_) {
        if (ctrl_ && !ctrl_->is_static) {
            ++ctrl_->ref_count;
        }
    }

    // Copy assignment: Release old control block, take new one
    StringSlice& operator=(const StringSlice& other) {
        if (this != &other) {
            // Clean up current control block
            if (ctrl_ && !ctrl_->is_static && --ctrl_->ref_count == 0) {
                delete[] ctrl_->data;
                delete ctrl_;
            }
            // Take other's control block
            ctrl_ = other.ctrl_;
            start_ = other.start_;
            length_ = other.length_;
            if (ctrl_ && !ctrl_->is_static) {
                ++ctrl_->ref_count;
            }
        }
        return *this;
    }

    // Move constructor: Transfer ownership without ref count changes
    StringSlice(StringSlice&& other) noexcept
        : ctrl_(other.ctrl_), start_(other.start_), length_(other.length_) {
        other.ctrl_ = nullptr;
    }

    // Move assignment: Transfer ownership
    StringSlice& operator=(StringSlice&& other) noexcept {
        if (this != &other) {
            // Clean up current control block
            if (ctrl_ && !ctrl_->is_static && --ctrl_->ref_count == 0) {
                delete[] ctrl_->data;
                delete ctrl_;
            }
            // Take other's control block
            ctrl_ = other.ctrl_;
            start_ = other.start_;
            length_ = other.length_;
            other.ctrl_ = nullptr;
        }
        return *this;
    }
};

Optimizations & Best Practices

  • Atomic vs Non-Atomic Ref Count: If your use case is single-threaded, replace std::atomic<size_t> with a plain size_t for better performance. Only use atomic counts in multi-threaded scenarios.
  • Buffer Capacity Tuning: When copying during COW, you can allocate extra capacity (like length_ * 2) to avoid frequent reallocations if further modifications are expected.
  • Boundary Checks: The example includes basic error checking, but you can adjust this to return errors instead of throwing exceptions if your workflow prefers that.
  • Interoperability with std::string: Add a method like std::string to_string() const that copies the slice data into a std::string for compatibility with existing code.
  • Avoid Overhead for Static Strings: By marking static buffers, we skip unnecessary reference count operations and avoid trying to free read-only memory.

Existing Library Inspiration

While rolling your own implementation gives you full control, many high-performance C++ libraries have similar components:

  • absl::string_view is a non-owning slice, but you could wrap it with a reference-counted control block to add COW.
  • folly::StringPiece is another non-owning view, which can be extended with ownership tracking.

内容的提问来源于stack exchange,提问作者John Doughnut

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.20 12:29:12