C++实现自定义的`std::unique_ptr`/`std::shared_ptr`:适应嵌入式环境的内存限制

嵌入式环境下的智能指针:定制化 unique_ptrshared_ptr

大家好,今天我们来聊聊在嵌入式环境下如何实现自定义的智能指针,特别是 unique_ptrshared_ptr。嵌入式系统通常资源受限,标准库提供的智能指针可能因为内存占用、性能开销等原因不太适用。因此,根据实际需求定制化智能指针,可以更好地满足嵌入式环境的要求。

1. 嵌入式环境下智能指针的需求与挑战

嵌入式系统对资源有着严格的限制。内存通常较小,CPU 性能也相对较弱。在这种环境下使用标准库的 std::unique_ptrstd::shared_ptr 会遇到以下挑战:

  • 内存占用: 标准库的 shared_ptr 需要维护一个引用计数器,通常分配在堆上,增加了内存开销。在内存受限的嵌入式系统中,堆内存的分配和释放需要谨慎管理。
  • 性能开销: 引用计数的增加和减少操作,特别是在多线程环境下,需要进行原子操作,这会带来额外的性能开销。
  • 异常处理: 某些嵌入式系统可能禁用异常处理,而标准库的智能指针在构造和析构时可能会抛出异常。
  • 代码体积: 标准库的实现通常比较复杂,代码体积较大,这对于 Flash 空间有限的嵌入式系统来说也是一个问题。

因此,我们需要针对嵌入式环境的特点,定制化智能指针,在功能、性能和资源占用之间找到平衡。

2. 自定义 unique_ptr

unique_ptr 用于独占资源的所有权,一个资源只能被一个 unique_ptr 管理。它在资源管理方面非常高效,因为它不需要维护引用计数。

2.1 基本实现

以下是一个简单的 unique_ptr 的实现:

template <typename T>
class unique_ptr {
private:
    T* ptr;

public:
    // 构造函数
    unique_ptr(T* p = nullptr) : ptr(p) {}

    // 移动构造函数
    unique_ptr(unique_ptr&& other) : ptr(other.ptr) {
        other.ptr = nullptr;
    }

    // 赋值运算符 (移动赋值)
    unique_ptr& operator=(unique_ptr&& other) {
        if (this != &other) {
            reset(other.ptr);
            other.ptr = nullptr;
        }
        return *this;
    }

    // 析构函数
    ~unique_ptr() {
        delete ptr;
    }

    // 解引用运算符
    T& operator*() const {
        return *ptr;
    }

    // 箭头运算符
    T* operator->() const {
        return ptr;
    }

    // 获取原始指针
    T* get() const {
        return ptr;
    }

    // 释放所有权,返回原始指针
    T* release() {
        T* temp = ptr;
        ptr = nullptr;
        return temp;
    }

    // 重置指针
    void reset(T* p = nullptr) {
        delete ptr;
        ptr = p;
    }

    // 禁用拷贝构造函数和赋值运算符
    unique_ptr(const unique_ptr&) = delete;
    unique_ptr& operator=(const unique_ptr&) = delete;
};

2.2 定制化删除器

unique_ptr 可以使用自定义的删除器,这对于管理非 new 分配的资源非常有用。例如,可以使用自定义的删除器来释放通过 malloc 分配的内存,或者关闭文件句柄。

template <typename T, typename Deleter = std::default_delete<T>>
class unique_ptr {
private:
    T* ptr;
    Deleter deleter;

public:
    // 构造函数
    unique_ptr(T* p = nullptr, const Deleter& d = Deleter()) : ptr(p), deleter(d) {}

    // 移动构造函数
    unique_ptr(unique_ptr&& other) : ptr(other.ptr), deleter(std::move(other.deleter)) {
        other.ptr = nullptr;
    }

    // 赋值运算符 (移动赋值)
    unique_ptr& operator=(unique_ptr&& other) {
        if (this != &other) {
            reset(other.ptr);
            deleter = std::move(other.deleter); // 重要:移动deleter
            other.ptr = nullptr;
        }
        return *this;
    }

    // 析构函数
    ~unique_ptr() {
        deleter(ptr);
    }

    // 解引用运算符
    T& operator*() const {
        return *ptr;
    }

    // 箭头运算符
    T* operator->() const {
        return ptr;
    }

    // 获取原始指针
    T* get() const {
        return ptr;
    }

    // 释放所有权,返回原始指针
    T* release() {
        T* temp = ptr;
        ptr = nullptr;
        return temp;
    }

    // 重置指针
    void reset(T* p = nullptr) {
        deleter(ptr);
        ptr = p;
    }

    // 禁用拷贝构造函数和赋值运算符
    unique_ptr(const unique_ptr&) = delete;
    unique_ptr& operator=(const unique_ptr&) = delete;
};

示例:使用 mallocfree

struct MallocDeleter {
    void operator()(void* p) const {
        free(p);
    }
};

int main() {
    int* data = (int*)malloc(sizeof(int));
    unique_ptr<int, MallocDeleter> ptr(data);
    *ptr = 42;
    return 0;
}

2.3 嵌入式环境下的优化

  • 静态分配删除器: 如果删除器不依赖于运行时状态,可以将其声明为 static 成员函数或全局函数,避免在 unique_ptr 对象中存储删除器对象。
  • 禁用异常: 确保代码不抛出异常,或者使用 noexcept 说明符来标记构造函数、析构函数和赋值运算符。
  • 减少代码体积: 避免使用复杂的模板元编程技巧,尽量使用简单的代码实现。

3. 自定义 shared_ptr

shared_ptr 用于共享资源的所有权,多个 shared_ptr 可以指向同一个资源。当最后一个 shared_ptr 销毁时,资源才会被释放。

3.1 基本实现 (单线程)

以下是一个简单的 shared_ptr 的单线程实现:

template <typename T>
class shared_ptr {
private:
    T* ptr;
    size_t* count; // 引用计数

public:
    // 构造函数
    shared_ptr(T* p = nullptr) : ptr(p), count(p ? new size_t(1) : nullptr) {}

    // 拷贝构造函数
    shared_ptr(const shared_ptr& other) : ptr(other.ptr), count(other.count) {
        if (count) {
            (*count)++;
        }
    }

    // 赋值运算符 (拷贝赋值)
    shared_ptr& operator=(const shared_ptr& other) {
        if (this != &other) {
            // 减少当前对象的引用计数
            if (count && --(*count) == 0) {
                delete ptr;
                delete count;
            }

            // 增加新对象的引用计数
            ptr = other.ptr;
            count = other.count;
            if (count) {
                (*count)++;
            }
        }
        return *this;
    }

    // 析构函数
    ~shared_ptr() {
        if (count && --(*count) == 0) {
            delete ptr;
            delete count;
        }
    }

    // 解引用运算符
    T& operator*() const {
        return *ptr;
    }

    // 箭头运算符
    T* operator->() const {
        return ptr;
    }

    // 获取原始指针
    T* get() const {
        return ptr;
    }

    // 获取引用计数
    size_t use_count() const {
        return count ? *count : 0;
    }
};

3.2 多线程支持

在多线程环境下,引用计数的增加和减少操作需要是原子操作,以避免数据竞争。可以使用 std::atomic 来实现原子计数。

#include <atomic>

template <typename T>
class shared_ptr {
private:
    T* ptr;
    std::atomic<size_t>* count; // 原子引用计数

public:
    // 构造函数
    shared_ptr(T* p = nullptr) : ptr(p), count(p ? new std::atomic<size_t>(1) : nullptr) {}

    // 拷贝构造函数
    shared_ptr(const shared_ptr& other) : ptr(other.ptr), count(other.count) {
        if (count) {
            count->fetch_add(1, std::memory_order_relaxed); // 增加引用计数
        }
    }

    // 赋值运算符 (拷贝赋值)
    shared_ptr& operator=(const shared_ptr& other) {
        if (this != &other) {
            // 减少当前对象的引用计数
            if (count && count->fetch_sub(1, std::memory_order_release) == 1) {
                std::atomic_thread_fence(std::memory_order_acquire);
                delete ptr;
                delete count;
            }

            // 增加新对象的引用计数
            ptr = other.ptr;
            count = other.count;
            if (count) {
                count->fetch_add(1, std::memory_order_relaxed); // 增加引用计数
            }
        }
        return *this;
    }

    // 析构函数
    ~shared_ptr() {
        if (count && count->fetch_sub(1, std::memory_order_release) == 1) {
            std::atomic_thread_fence(std::memory_order_acquire);
            delete ptr;
            delete count;
        }
    }

    // 解引用运算符
    T& operator*() const {
        return *ptr;
    }

    // 箭头运算符
    T* operator->() const {
        return ptr;
    }

    // 获取原始指针
    T* get() const {
        return ptr;
    }

    // 获取引用计数
    size_t use_count() const {
        return count ? count->load(std::memory_order_relaxed) : 0;
    }
};

3.3 嵌入式环境下的优化

  • 避免动态内存分配: 可以考虑使用预分配的内存池来存储引用计数。
  • 使用轻量级原子操作: 某些嵌入式平台可能提供更轻量级的原子操作,可以替代 std::atomic。例如,可以使用无锁算法来实现引用计数。
  • 限制最大引用计数: 可以限制 shared_ptr 的最大引用计数,以减少计数器的存储空间。
  • 考虑使用 intrusive_ptr: intrusive_ptr 将引用计数嵌入到被管理的对象中,可以避免额外的内存分配。 但是需要修改被管理对象的结构。

3.4 intrusive_ptr 的实现

template <typename T>
class intrusive_ptr {
private:
    T* ptr;

public:
    // 构造函数
    intrusive_ptr(T* p = nullptr) : ptr(p) {
        if (ptr) {
            intrusive_ptr_add_ref(ptr);
        }
    }

    // 拷贝构造函数
    intrusive_ptr(const intrusive_ptr& other) : ptr(other.ptr) {
        if (ptr) {
            intrusive_ptr_add_ref(ptr);
        }
    }

    // 赋值运算符
    intrusive_ptr& operator=(const intrusive_ptr& other) {
        if (this != &other) {
            intrusive_ptr_release(ptr);
            ptr = other.ptr;
            if (ptr) {
                intrusive_ptr_add_ref(ptr);
            }
        }
        return *this;
    }

    // 析构函数
    ~intrusive_ptr() {
        intrusive_ptr_release(ptr);
    }

    // 解引用运算符
    T& operator*() const {
        return *ptr;
    }

    // 箭头运算符
    T* operator->() const {
        return ptr;
    }

    // 获取原始指针
    T* get() const {
        return ptr;
    }
};

// 需要在被管理的对象中提供以下两个函数
void intrusive_ptr_add_ref(MyObject* p);
void intrusive_ptr_release(MyObject* p);

// 示例
class MyObject {
public:
    std::atomic<int> ref_count;

    MyObject() : ref_count(0) {}

    ~MyObject() {
        // 在对象销毁时执行清理操作
    }
};

void intrusive_ptr_add_ref(MyObject* p) {
    p->ref_count.fetch_add(1, std::memory_order_relaxed);
}

void intrusive_ptr_release(MyObject* p) {
    if (p && p->ref_count.fetch_sub(1, std::memory_order_release) == 1) {
        std::atomic_thread_fence(std::memory_order_acquire);
        delete p;
    }
}

int main() {
    MyObject* obj = new MyObject();
    intrusive_ptr<MyObject> ptr1(obj);
    intrusive_ptr<MyObject> ptr2(ptr1);

    return 0;
}

4. 总结表格

特性 std::unique_ptr 自定义 unique_ptr std::shared_ptr 自定义 shared_ptr intrusive_ptr
所有权 独占 独占 共享 共享 共享
引用计数 有 (对象内)
内存占用 较小 较小 较大 可优化 较小
性能 较低 可优化
异常安全 较高 需保证 较高 需保证 需保证
适用场景 独占资源管理 独占资源管理 共享资源管理 共享资源管理 共享资源管理
嵌入式环境优化 使用自定义删除器 静态分配删除器,禁用异常 预分配内存池,轻量级原子操作 预分配内存池,轻量级原子操作 嵌入引用计数
代码复杂性 中等 较低 中等 可优化 中等

5. 总结:定制化智能指针,平衡资源与功能

在嵌入式环境下,标准库的智能指针可能并不总是最佳选择。通过定制化 unique_ptrshared_ptr,可以更好地适应资源限制,提高性能,并确保代码的可靠性。 关键在于理解嵌入式环境的特点,并根据实际需求进行优化。
通过对删除器,内存分配方式和线程同步策略的调整,我们可以实现更高效、更可靠的智能指针,从而提升嵌入式系统的整体性能。
考虑使用 intrusive_ptr 在特定场景下可以减少内存分配的开销,但是需要对目标对象进行修改。

更多IT精英技术系列讲座,到智猿学院

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注