在〈auto_ptr〉中,主要是認識自動管理動態配置物件的原理,c++ 98 的 auto_ptr
被廢棄的原因顯而易見,往往一個不小心,就忽略了資源被接管的問題,另一個問題是,它無法管理動態配置的連續空間,因為不會使用 delete []
來刪除。
對於第一個問題,主要原因來自於複製時就會發生資源接管,既然如此,就禁止複製吧!這可以將複製建構式與複製指定運算子刪掉來達到,不過,實際上還是會需要轉移資源權,那麼就明確地定義釋放資源與重置資源的方法;對於第二個問題,可以讓使用者指定刪除器,自行決定怎麼刪除資源。
實際上 C++ 11 的標準程式庫在 memory
標頭檔,定義有 unique_ptr
實現了以上的概念,不過試著自行實現個基本版本,是個不錯的挑戰,也能對 unique_ptr
有更多認識,那就來看個基本的版本吧!
#include <iostream>
#include<utility>
#include <functional>
using namespace std;
class Deleter {
public:
template <typename T>
void operator()(T* ptr) { delete ptr; }
};
// 預設的 D 型態是 Deleter
template<typename T, typename D = Deleter>
class UniquePtr {
T* p;
D del;
public:
// 不能複製
UniquePtr(const UniquePtr<T>&) = delete;
UniquePtr<T>& operator=(const UniquePtr<T>&) = delete;
UniquePtr() = default;
// 每個 UniquePtr 有自己的 Deleter
UniquePtr(T* p, const D &del = D()) : p(p), del(del) {}
// 對於右值可以直接進行資源的移動
UniquePtr(UniquePtr<T>&& uniquePtr) : p(uniquePtr.p), del(std::move(uniquePtr.del)) {
uniquePtr.p = nullptr;
}
UniquePtr<T>& operator=(UniquePtr<T> &&uniquePtr) {
if(this != &uniquePtr) {
this->reset();
this->p = uniquePtr.p;
del = std::move(uniquePtr.del);
uniquePtr.p = nullptr;
}
return *this;
}
~UniquePtr() {
del(this->p);
}
// 釋放資源的管理權
T* release() {
T* r = this->p;
this->p = nullptr;
return r;
}
// 重設管理的資源
void reset(T *p = nullptr) {
del(this->p);
this->p = p;
}
// 令 UniquePtr 行為像個指標
T& operator*() { return *(this->p); }
T* operator->() { return this->p; }
};
...未完
來從實際的使用中認識這個實作:
...略
class Foo {
public:
int n;
Foo(int n) : n(n) {}
~Foo() {
cout << n << " Foo deleted" << endl;
}
};
int main() {
UniquePtr<Foo> f1(new Foo(10));
UniquePtr<Foo> f2(new Foo(20));
f2.reset(f1.release());
return 0;
}
因為無法複製了,在上例中,你不能 UniquePtr<Foo> f2 = f1
,或者是 f2 = f1
,因此不會隱含地就轉移了資源的管理權,然而,可以透過 release
本身釋放資源,f1.release()
後不再管理原本的資源,資源的位址被傳回,透過 f2
的 reset
設定給 f2
,f2
原本的資源會被刪除,管理的資源被設定為接收到的資源,透過 release
與 reset
,資源的轉移得到了明確的語義。
因為無法複製了,你不能將 UniquePtr
實例作為引數傳入函式;然而,這邊看到了 rvalue
運算式與 std::move
的一個應用,當 UniquePtr
實例作為傳回值時,雖然呼叫者會建立新的 UniquePtr
實例,然而因為實作了移動建構式與移動指定運算子,被傳回的 UniquePtr
實際上進行了資源的移動,結果就是,你可以從函式中傳回 UniquePtr
實例。例如:
...
auto unique_foo(int n) {
return UniquePtr<Foo>(new Foo(n));
}
int main() {
auto foo = unique_foo(10);
cout << foo->n << endl;
return 0;
}
這個範例的意思就是,既然自動管理資源了,就透過 unique_foo
避免使用 new
吧!如果要管理動態配置的連續空間呢?
...略
auto unique_arr(int len) {
auto deleter = [](int *arr) { delete [] arr; };
return UniquePtr<int, decltype(deleter)>(new int[len] {0}, deleter);
}
int main() {
auto arr = unique_arr(10);
cout << *arr << endl;
return 0;
}
透過自訂的刪除器,就可以指定如何刪除動態配置的連續空間了,當然,這邊實作的 UniquePtr
並不全面,因為沒有重載下標運算子,因此無法如陣列可以使用下標操作。
來看看標準程式庫的 unique_ptr
怎麼用吧!
#include <iostream>
#include<memory>
#include <functional>
using namespace std;
class Foo {
public:
int n;
Foo(int n) : n(n) {}
~Foo() {
cout << n << " Foo deleted" << endl;
}
};
int main() {
unique_ptr<Foo> f1(new Foo(10));
unique_ptr<Foo> f2(new Foo(20));
f2.reset(f1.release());
return 0;
}
C++ 11 時要以 new
建立 unique_ptr
,這是制定規範時的疏忽,從 C++ 14 開始,建議使用 make_unique
,這可以避免直接使用 new
:
#include <iostream>
#include<memory>
#include <functional>
using namespace std;
class Foo {
public:
int n;
Foo(int n) : n(n) {}
~Foo() {
cout << n << " Foo deleted" << endl;
}
};
int main() {
auto f1 = make_unique<Foo>(10);
auto f2 = make_unique<Foo>(20);
f2.reset(f1.release());
return 0;
}
C++ 11 沒有 make_unique
,不過可以自行實作:
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args)
{
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
這個版本的 make_unique
指定的引數,都會用於建構實例,如果是動態配置連續空間呢?C++ 11 時,為此準備了另一個版本的 unique_ptr
,支援下標運算子,例如:
#include <iostream>
#include<memory>
using namespace std;
int main() {
unique_ptr<int[]> arr(new int[3] {1, 2, 3});
for(auto i = 0; i < 3; i++) {
cout << arr[i] << endl;
}
return 0;
}
這個版本不用指定刪除器,在 unique_ptr
生命週期結束時,會自動刪除動態配置的連續空間,make_unique
有個對應的重載版本,可以指定動態配置的長度:
#include <iostream>
#include<memory>
using namespace std;
int main() {
auto arr = make_unique<int[]>(3);
for(auto i = 0; i < 3; i++) {
cout << arr[i] << endl;
}
return 0;
}
雖然可以如下動態配置連續空間,也可以自行指定刪除器,然而意義不大就是了:
#include <iostream>
#include<memory>
using namespace std;
int main() {
auto deleter = [](int *arr) { delete [] arr; };
unique_ptr<int, decltype(deleter)> arr(new int[2] {0, 1}, deleter);
cout << *arr << endl;
return 0;
}
在這個範例中,並不能對 arr
下標操作,也不能對 arr
進行加、減操作,因為並沒有重載對應的運算子,這也說明了一件事,雖然許多文件會稱 unique_arr
或之後要談到的 shared_ptr
等為智慧指標(smart pointer),然而這並不正確,因為從這篇文件一開始,其實就知道,unique_arr
等型態的實例並不是指標,它只是有指標部份行為罷了。
理解這個事實後,對於動態配置連續空間這件事,並想要以下標操作應該先前使用使用 unique_ptr
或 make_unique
的對應版本。
支援下標運算子版本的 unique_ptr
,也可以自訂刪除器:
#include <iostream>
#include<memory>
using namespace std;
int main() {
auto deleter = [](int arr[]) { delete [] arr; };
unique_ptr<int[], decltype(deleter)> arr(new int[3] {1, 2, 3}, deleter);
for(auto i = 0; i < 3; i++) {
cout << arr[i] << endl;
}
return 0;
}
那麼 make_unique
可否指定刪除器呢?基本上 make_unique
是為了不需要自訂刪除器的場合而存在的,因為指定了刪除器,代表著你會使用 delete
,這就表示也必須對應的 new
存在,另外,由於支援下標操作的版本存在,自訂刪除器的需求也減少了,若還是有需求,就直接在建構 unique_ptr
時指定。