Bootstrap

Modern Effective C++ 条款十八:对于独占资源使用std::unique_ptr

条款十八:对于独占资源使用std::unique_ptr

 当需要一个智能指针时,std::unique_ptr通常是最合适的。默认情况下,std::unique_ptr大小等同于原始指针,而且对于大多数操作(包括取消引用),他们执行的指令完全相同。甚至可以在内存和时间都比较紧张的情况下使用它。如果原始指针够小够快,那么std::unique_ptr一样可以。

std::unique_ptr体现了专有所有权(exclusive ownership)语义。一个non-null std::unique_ptr始终拥有其指向的内容。移动一个std::unique_ptr将所有权从源指针转移到目的指针。(源指针被设为null。)拷贝一个std::unique_ptr是不允许的,因为如果你能拷贝一个std::unique_ptr,会得到指向相同内容的两个std::unique_ptr,每个都认为自己拥有(并且应当最后销毁)资源,销毁时就会出现重复销毁。因此,std::unique_ptr是一种只可移动类型(move-only type)。当析构时,一个non-null std::unique_ptr销毁它指向的资源。默认情况下,资源析构通过对std::unique_ptr里原始指针调用delete来实现。
std::unique_ptr的常见用法是作为继承层次结构中对象的工厂函数返回类型。假设有一个投资类型(的继承结构,使用基类Investment。

class Investment{ … };
class Stock:public Investment { … };
class Bond:public Investment { … };

class RealEstate:public Investment { … };使用std::unique_ptr智能指针来管理工厂函数返回的对象,并且在需要时提供自定义的删除器。

工厂函数
工厂函数用于在堆上分配一个对象并返回指向该对象的std::unique_ptr。调用者对工厂返回的资源负责,即拥有专有所有权。std::unique_ptr确保当其自身被销毁时,所指向的对象也会被自动销毁。
template<typename… Ts>
std::unique_ptr makeInvestment(Ts&&… params);模板参数包:Ts&&… params允许传递任意数量和类型的参数。
返回类型:std::unique_ptr,表示返回一个指向Investment基类的智能指针。
调用工厂函数

{
    auto pInvestment = makeInvestment(arguments);
    // 使用 pInvestment
} // pInvestment 在作用域结束时自动销毁pInvestment在其作用域结束时会自动销毁,并释放其所拥有的资源。

自定义删除器

auto delInvmt = [](Investment* pInvestment) {
    makeLogEntry(pInvestment); // 记录日志
    delete pInvestment;        // 删除对象
};使用lambda表达式,作为自定义删除器。在删除对象之前记录日志,然后通过delete释放资源。

修改后的工厂函数

template<typename... Ts>
std::unique_ptr<Investment, decltype(delInvmt)> makeInvestment(Ts&&... params)
{
    std::unique_ptr<Investment, decltype(delInvmt)> pInv(nullptr, delInvmt);
    if (/* 创建Stock对象 */) {
        pInv.reset(new Stock(std::forward<Ts>(params)...));
    } else if (/* 创建Bond对象 */) {
        pInv.reset(new Bond(std::forward<Ts>(params)...));
    } else if (/* 创建RealEstate对象 */) {
        pInv.reset(new RealEstate(std::forward<Ts>(params)...));
    }
    return pInv;
}

返回类型:std::unique_ptr<Investment, decltype(delInvmt)>,使用自定义删除器delInvmt。pInv初始化为空指针,并关联自定义删除器delInvmt。
创建对象:根据条件创建不同的派生类对象(如Stock、Bond或RealEstate),并通过reset方法将所有权转移给pInv。
完美转发:使用std::forward将参数完美转发给构造函数,保留参数的值类别。
class Investment {
public:
virtual ~Investment(); // 关键设计部分!
// 其他成员函数
};虚析构函数:virtual ~Investment()确保通过基类指针删除派生类对象时,派生类的析构函数会被正确调用。
C++14中函数返回类型推导的引入使得makeInvestment工厂函数可以以更简洁和封装的方式实现。使用C++14的返回类型推导

template<typename... Ts>
auto makeInvestment(Ts&&... params) // C++14
{
    auto delInvmt = [](Investment* pInvestment) { // 自定义删除器
        makeLogEntry(pInvestment);
        delete pInvestment;
    };
    std::unique_ptr<Investment, decltype(delInvmt)> pInv(nullptr, delInvmt);
    if (/* 创建Stock对象 */) {
        pInv.reset(new Stock(std::forward<Ts>(params)...));
    } else if (/* 创建Bond对象 */) {
        pInv.reset(new Bond(std::forward<Ts>(params)...));
    } else if (/* 创建RealEstate对象 */) {
        pInv.reset(new RealEstate(std::forward<Ts>(params)...));
    }
    return pInv; // 返回带有自定义删除器的std::unique_ptr
}

返回类型推导:auto关键字用于让编译器自动推导makeInvestment的返回类型。
自定义删除器:
delInvmt是一个lambda表达式,作为自定义删除器。在删除Investment对象之前,它会记录日志并调用delete来释放资源。
std::unique_ptr的构造:
pInv初始化为空指针,并关联自定义删除器delInvmt。
decltype(delInvmt)用于获取delInvmt的类型,确保std::unique_ptr使用正确的删除器类型。
创建对象:
根据条件创建不同的派生类对象(如Stock、Bond或RealEstate)。使用reset方法将所有权转移给pInv,并通过std::forward完美转发参数。
返回智能指针:函数返回带有自定义删除器的std::unique_ptr。

关于std::unique_ptr的大小

默认删除器:当使用默认删除器(即delete)时,std::unique_ptr的大小通常与原始指针相同。
自定义删除器:函数指针形式的删除器通常会使std::unique_ptr的大小从一个字(word)增加到两个字。
如果自定义删除器是无状态的lambda表达式(不捕获任何变量),则对std::unique_ptr的大小没有影响。如果有状态的删除器(例如捕获了变量的lambda),则std::unique_ptr的大小会增加,具体取决于捕获的状态量。
示例:无状态lambda vs 函数指针

// 无状态lambda
auto delInvmt1 = [](Investment* pInvestment) {
    makeLogEntry(pInvestment);
    delete pInvestment;
};
// 函数指针
void delInvmt2(Investment* pInvestment) {
    makeLogEntry(pInvestment);
    delete pInvestment;
}

无状态lambda:delInvmt1不会增加std::unique_ptr的大小。
函数指针:delInvmt2会增加std::unique_ptr的大小,至少增加一个函数指针的大小。
其他用途
Pimpl Idiom:std::unique_ptr常用于实现Pimpl Idiom,隐藏实际实现以减少编译依赖性。
数组管理:std::unique_ptr<T[]>用于管理动态分配的数组,但通常建议使用std::array、std::vector或std::string等容器代替原始数组。
(1) std::unique_ptr<T[]> 是一个智能指针,专门用于管理动态分配的数组。它确保在智能指针生命周期结束时自动释放数组内存,提供对数组的独占所有权。使用 operator[] 来访问数组元素。调用delete[]而不是delete来释放内存。

int main() {
    std::unique_ptr<int[]> arr(new int[5]{1, 2, 3, 4, 5});
    for (int i = 0; i < 5; ++i) {
        std::cout << arr[i] << " ";
    }
}

(2)std::array 是一个固定大小的数组容器,它封装了 C 风格的数组,并提供了额外的安全性和便利性。大小在编译时确定且不可更改。不会发生越界访问(通过 at 方法)。提供了标准容器接口,如 begin、end、size 等。

int main() {
    std::array<int, 5> arr = {1, 2, 3, 4, 5};
    for (const auto& elem : arr) {
        std::cout << elem << " ";
    }

}(2) std::vector 是动态数组,见STL。
推荐使用 std::array、std::vector 或 std::string。std::array 和 std::vector 提供了边界检查,避免了越界访问。std::string 提供了各种字符串操作方法,减少了手动处理字符串的错误。这些容器提供了丰富的操作方法,简化了代码编写。
std::unique_ptr可以轻松转换为std::shared_ptr,这使得工厂函数返回std::unique_ptr非常灵活。
如果调用者需要共享所有权,可以直接将std::unique_ptr转换为std::shared_ptr。
std::shared_ptr sp = makeInvestment(arguments);这种灵活性使得工厂函数能够提供最高效的智能指针,同时不妨碍调用者根据需要选择更适合的智能指针类型。 工厂函数无法知道调用者是否要对它们返回的对象使用专有所有权语义,或者共享所有权(即std::shared_ptr)是否更合适。 通过返回std::unique_ptr,工厂为调用者提供了最有效的智能指针,但它们并不妨碍调用者用其更灵活的兄弟替换它。
请记住:
• std::unique_ptr是轻量级、快速的、只可移动(move-only)的管理专有所有权语义资源的智能指针
• 默认情况,资源销毁通过delete实现,但是支持自定义删除器。有状态的删除器和函数指针会增加std::unique_ptr对象的大小
• 将std::unique_ptr转化为std::shared_ptr非常简单

;