![](/img/trans.png)
[英]recreate(reassign) a std::shared_ptr or std::unique_ptr
[英]What's the advantage of `std::optional` over `std::shared_ptr` and `std::unique_ptr`?
的推理std::optional
的說發,它可能會或可能不會包含值。 因此,如果我們不需要它,它可以節省我們構建一個可能是大對象的工作。
例如,這里的工廠,如果不滿足某些條件,則不會構造對象:
#include <string>
#include <iostream>
#include <optional>
std::optional<std::string> create(bool b)
{
if(b)
return "Godzilla"; //string is constructed
else
return {}; //no construction of the string required
}
但是,這與此有何不同:
std::shared_ptr<std::string> create(bool b)
{
if(b)
return std::make_shared<std::string>("Godzilla"); //string is constructed
else
return nullptr; //no construction of the string required
}
我們通過添加std::optional
不是一般只使用std::shared_ptr
贏得什么?
我們通過添加 std::optional 而不是一般只使用 std::shared_ptr 會贏得什么?
假設您需要從帶有“非值”標志的函數中返回一個符號。 如果您為此使用std::shared_ptr
,則會產生巨大的開銷 - char
將在動態內存中分配,加上std::shared_ptr
將維護控制塊。 而std::optional在另一邊:
如果一個可選項包含一個值,則保證該值作為可選對象占用空間的一部分進行分配,即永遠不會發生動態內存分配。 因此,即使定義了 operator*() 和 operator->() ,可選對象也建模對象,而不是指針。
因此不涉及動態內存分配,甚至與原始指針相比的差異也可能很大。
可選是可空值類型。
shared_ptr
是可以為空的引用計數引用類型。
unique_ptr
是可以為空的僅移動引用類型。
它們的共同點是它們可以為空——它們可以“不存在”。
它們的不同之處在於,兩個是引用類型,另一個是值類型。
值類型有幾個優點。 首先,它不需要在堆上分配——它可以與其他數據一起存儲。 這消除了可能的異常來源(內存分配失敗),可以更快(堆比堆棧慢),並且對緩存更友好(因為堆往往是相對隨機排列的)。
引用類型還有其他優點。 移動引用類型不需要移動源數據。
對於僅限非移動的引用類型,您可以通過不同的名稱對同一數據進行多個引用。 具有不同名稱的兩種不同的值類型總是引用不同的數據。 無論哪種方式,這都可能是優勢或劣勢; 但它確實使有關值類型的推理變得更加容易。
關於shared_ptr
推理非常困難。 除非對其使用方式進行非常嚴格的控制,否則幾乎不可能知道數據的生命周期是多少。 關於unique_ptr
推理要容易得多,因為您只需要跟蹤它移動的位置。 關於optional
的生命周期的推理是微不足道的(好吧,就像你嵌入的內容一樣微不足道)。
可選接口增加了一些類似.value_or
方法(如.value_or
),但這些方法通常可以很容易地添加到任何可空類型中。 盡管如此,目前,它們是optional
而不是shared_ptr
或unique_ptr
。
optional 的另一個巨大好處是,您非常清楚有時希望它可以為空。 有C ++中的壞習慣推測指針和智能指針不為空,因為它們用於比做為空的其他原因。
所以代碼假設一些共享或唯一的 ptr 永遠不會為空。 它通常有效。
相比之下,如果你有一個可選項,你擁有它的唯一原因是因為它實際上有可能為空。
在實踐中,我對將unique_ptr<enum_flags> = nullptr
作為參數持懷疑態度,我想說“這些標志是可選的”,因為強制調用者分配堆似乎很粗魯。 但是optional<enum_flags>
不會強制調用者執行此操作。 optional
廉價性使我願意在許多情況下使用它,如果我唯一的可為空類型是智能指針,我會找到其他一些解決方法。
這消除了很多“標志值”的誘惑,比如int rows=-1;
. optional<int> rows;
具有更清晰的含義,並且在調試中會告訴我何時使用行而不檢查“空”狀態。
可以合理失敗或不返回任何感興趣的東西的函數可以避免標志值或堆分配,並返回optional<R>
。 例如,假設我有一個可放棄的線程池(例如,一個在用戶關閉應用程序時停止處理的線程池)。
我可以從“隊列任務”函數返回std::future<R>
並使用異常來指示線程池已被放棄。 但這意味着線程池的所有使用都必須針對“來自”異常代碼流進行審計。
相反,我可以返回std::future<optional<R>>
,並提示用戶他們必須在他們的邏輯中處理“如果過程從未發生過會發生什么”。
“來自”異常仍然可能發生,但它們現在是異常的,不是標准關閉程序的一部分。
在其中一些情況下, expected<T,E>
將是一個更好的解決方案,一旦它在標准中。
指針可能是也可能不是 NULL。 這對你來說是否意味着什么完全取決於你。 在某些情況下, nullptr
是您處理的有效值,而在其他情況下,它可以用作指示“沒有值,繼續前進”的標志。
使用std::optional
,有“包含值”和“不包含值”的明確定義。 您甚至可以使用帶有 optional 的指針類型!
這是一個人為的例子:
我有一個名為Person
的類,我想從磁盤延遲加載他們的數據。 我需要指出是否已加載某些數據。 讓我們為此使用一個指針:
class Person
{
mutable std::unique_ptr<std::string> name;
size_t uuid;
public:
Person(size_t _uuid) : uuid(_uuid){}
std::string GetName() const
{
if (!name)
name = PersonLoader::LoadName(uuid); // magic PersonLoader class knows how to read this person's name from disk
if (!name)
return "";
return *name;
}
};
太好了,我可以使用nullptr
值來判斷名稱是否已從磁盤加載。
但是如果一個字段是可選的呢? 也就是說, PersonLoader::LoadName()
可能會為此人返回nullptr
。 每次有人請求這個名字時,我們真的想要去磁盤嗎?
輸入std::optional
。 現在我們可以跟蹤是否已經嘗試加載名稱以及該名稱是否為空。 如果沒有std::optional
,對此的解決方案是為名稱創建一個布爾值isLoaded
,實際上是每個可選字段。 (如果我們“只是將標志封裝到一個結構體中”怎么辦?好吧,那么您已經實現了optional
,但做得更糟):
class Person
{
mutable std::optional<std::unique_ptr<std::string>> name;
size_t uuid;
public:
Person(size_t _uuid) : uuid(_uuid){}
std::string GetName() const
{
if (!name){ // need to load name from disk
name = PersonLoader::LoadName(uuid);
}
// else name's already been loaded, retrieve cached value
if (!name.value())
return "";
return *name.value();
}
};
現在我們不需要每次都去磁盤了; std::optional
允許我們檢查它。 我在評論中寫了一個小例子,以較小的規模展示這個概念
重要的是,如果您嘗試從不存在的可選項中訪問value()
,則會得到一個已知的、可捕獲的異常而不是未定義的行為。 因此,如果optional
出現問題,與使用shared_ptr
等相比,您的調試時間可能要好得多。 (請注意,在這種情況下, optional
上的*
取消引用運算符仍然給出 UB;使用value()
是更安全的選擇)。
此外,還有value_or
等方法的一般便利性,它允許您很容易地指定“默認”值。 相比:
(t == nullptr) ? "default" : *t
和
t.value_or("default")
后者更易讀,而且略短。
最后, optional
項目的存儲在對象內。 這意味着如果對象不存在,則可optional
需要比指針更多的存儲空間; 然而,這也意味着不需要動態分配將對象放入空的optional
。
我們通過添加 std::optional 而不是一般只使用 std::shared_ptr 會贏得什么?
@Slava 提到了不執行內存分配的優勢,但這是一個附帶的好處(好吧,在某些情況下它可能是一個顯着的好處,但我的意思是,它不是主要的好處)。
主要好處是(恕我直言)更清晰的語義:
返回指針通常意味着(在現代 C++ 中)“分配內存”,或“處理內存”,或“知道這個和那個內存中的地址”。
返回一個可選值意味着“沒有這個計算的結果,不是錯誤”:返回類型的名稱,告訴你一些關於 API 是如何構思的(API 的意圖,而不是實現)。
理想情況下,如果您的 API 不分配內存,則不應返回指針。
在標准中提供可選類型,確保您可以編寫更具表現力的 API。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.