[英]std::unordered_map<T,std::unique_ptr<U>> copyable? GCC bug?
[英]Using std::unique_ptr of a polymorphic class as key in std::unordered_map
我的問題來自一個我應該完成的項目。 我必須創建一個std::unordered_map<T, unsigned int>
,其中T
是一個指向基礎多態 class 的指針。 過了一會兒,我認為使用std::unique_ptr<T>
作為鍵也是一個好習慣,因為我的 map 是為了擁有這些對象。 讓我介紹一些背景故事:
考慮 class 層次結構,以多態sell_obj作為基礎 class。 book和table繼承自 class。 我們現在知道我們需要創建一個std::unordered_map<std::unique_ptr<sell_obj*>, unsigned int>
。 因此,從 map 中擦除一對將自動釋放鍵指向的 memory。 整個想法是讓鑰匙指向書籍/桌子,這些鑰匙的價值將代表我們商店包含的該產品的數量。
當我們處理std::unordered_map
時,我們應該為所有三個類指定哈希值。 為了簡化事情,我在main中指定了它們,如下所示:
namespace std{
template <> struct hash<book>{
size_t operator()(const book& b) const
{
return 1; // simplified
}
};
template <> struct hash<table>{
size_t operator()(const table& b) const
{
return 2; // simplified
}
};
// The standard provides a specilization so that std::hash<unique_ptr<T>> is the same as std::hash<T*>.
template <> struct hash<sell_obj*>{
size_t operator()(const sell_obj *s) const
{
const book *b_p = dynamic_cast<const book*>(s);
if(b_p != nullptr) return std::hash<book>()(*b_p);
else{
const table *t_p = static_cast<const table*>(s);
return std::hash<table>()(*t_p);
}
}
};
}
現在讓我們看看 map 的實現。 我們有一個名為Shop的 class,如下所示:
#include "sell_obj.h"
#include "book.h"
#include "table.h"
#include <unordered_map>
#include <memory>
class Shop
{
public:
Shop();
void add_sell_obj(sell_obj&);
void remove_sell_obj(sell_obj&);
private:
std::unordered_map<std::unique_ptr<sell_obj>, unsigned int> storeroom;
};
和實現兩個關鍵功能:
void Shop::add_sell_obj(sell_obj& s_o)
{
std::unique_ptr<sell_obj> n_ptr(&s_o);
storeroom[std::move(n_ptr)]++;
}
void Shop::remove_sell_obj(sell_obj& s_o)
{
std::unique_ptr<sell_obj> n_ptr(&s_o);
auto target = storeroom.find(std::move(n_ptr));
if(target != storeroom.end() && target->second > 0) target->second--;
}
在我的主要我嘗試運行以下代碼:
int main()
{
book *b1 = new book("foo", "bar", 10);
sell_obj *ptr = b1;
Shop S_H;
S_H.add_sell_obj(*ptr); // works fine I guess
S_H.remove_sell_obj(*ptr); // usually (not always) crashes [SIGSEGV]
return 0;
}
我的問題是 - 我的邏輯在哪里失敗? 我聽說自 C++11 以來在 STL 容器中使用std::unique_ptr
很好。 是什么導致了崩潰? 除了崩潰發生之外,調試器不提供任何信息。
如果需要有關該項目的更多信息,請指出。 感謝您閱讀
這個問題的邏輯有很多問題。 首先:
考慮 class 層次結構,以多態
sell_obj
作為基礎 class。book
和table
繼承自 class。 我們現在知道我們需要創建一個std::unordered_map<std::unique_ptr<sell_obj*>, unsigned int>
。
在這種情況下std::unique_ptr<sell_obj*>
不是我們想要的。 我們想要std::unique_ptr<sell_obj>
。 沒有*
。 std::unique_ptr
已經是“指針”。
當我們處理
std::unordered_map
時,我們應該為所有三個類指定哈希值。 為了簡化事情,我在 main 中指定了它們,如下所示:[...]
這也是相當不受歡迎的做法。 這將需要每次我們在層次結構中添加另一個子類時更改那部分代碼。 最好以多態方式委托散列(和比較)以避免此類問題,正如@1201programalarm 所建議的那樣。
[...] 實現兩個關鍵功能:
void Shop::add_sell_obj(sell_obj& s_o) { std::unique_ptr<sell_obj> n_ptr(&s_o); storeroom[std::move(n_ptr)]++; } void Shop::remove_sell_obj(sell_obj& s_o) { std::unique_ptr<sell_obj> n_ptr(&s_o); auto target = storeroom.find(std::move(n_ptr)); if(target.= storeroom;end() && target->second > 0) target->second-- }
這是錯誤的,有幾個原因。 首先,通過非const
引用作為參數,建議修改 object。 其次,從通過在 argumnet 上使用&
獲得的指針創建 n_ptr n_ptr
非常危險的。 它假定 object 在堆上分配並且它是無主的。 這種情況通常不應該發生並且非常危險。 如果傳遞的 object 在堆棧上和/或已由其他所有者管理,這將導致災難(如段錯誤)。
更重要的是,它或多或少肯定會以災難告終,因為add_sell_obj add_sell_obj()
和remove_sell_obj()
都為潛在的相同 object 創建std::unique_ptr
。 這正是原始問題的main()
的情況。 兩個指向同一個 object 的std::unique_ptr
會導致雙重delete
。
雖然使用 C++(與 Java 相比)不一定是解決此問題的最佳方法,但有幾個有趣的工具可用於此任務。 下面的代碼假定 C++20。
首先,我們需要一個基礎 class ,用於引用存儲在商店中的所有對象:
struct sell_object { };
然后我們需要引入代表具體對象的類:
class book : public sell_object {
std::string title;
public:
book(std::string title) : title(std::move(title)) { }
};
class table : public sell_object {
int number_of_legs = 0;
public:
table(int number_of_legs) : number_of_legs(number_of_legs) { }
};
為簡單起見(但仍然有一些區別),我選擇讓它們只有一個不同的字段( title
和number_of_legs
)。
shop
class 將代表任何sell_object
的存儲,需要以某種方式存儲任何sell_object
。 為此,我們需要使用指向基礎 class 的指針或引用。 你不能有一個引用容器,所以最好使用指針。 智能指針。
最初問題建議使用std::unordered_map
。 讓我們堅持下去:
class shop {
std::unordered_map<
std::unique_ptr<sell_object>, int,
> storage;
public:
auto add(...) -> void {
...
}
auto remove(...) -> void {
...
}
};
值得一提的是,我們選擇std::unique_ptr
作為 map 的鍵。 這意味着存儲將復制傳遞的對象並使用它擁有的副本與我們查詢的元素進行比較(添加或刪除)。 但是,不會復制超過一個相等的 object。
然而,有一個問題。 std::unordered_map
使用散列,我們需要為std::unique_ptr<sell_object>
提供 hash 策略。 好吧,已經有一個,它使用 hash 策略T*
。 問題是我們想要自定義散列。 那些特定的std::unique_ptr<sell_object>
s 應該根據關聯的sell_object
s 進行散列。
因此,我選擇選擇與問題中提出的方法不同的方法。 我不會在std
命名空間中提供全局特化,而是選擇自定義散列 object 和自定義比較器:
class shop {
struct sell_object_hash {
auto operator()(std::unique_ptr<sell_object> const& object) const -> std::size_t {
return object->hash();
}
};
struct sell_object_equal {
auto operator()(
std::unique_ptr<sell_object> const& lhs,
std::unique_ptr<sell_object> const& rhs
) const -> bool {
return (*lhs <=> *rhs) == 0;
}
};
std::unordered_map<
std::unique_ptr<sell_object>, int,
sell_object_hash, sell_object_equal
> storage;
public:
auto add(...) -> void {
...
}
auto remove(...) -> void {
...
}
};
注意幾件事。 首先, storage
的類型發生了變化。 它不再是std::unordered_map<std::unique_ptr<T>, int>
,而是std::unordered_map<std::unique_ptr<T>, int, sell_object_hash, sell_object_equal>
。 這表明我們正在使用自定義哈希( sell_object_hash
)和自定義比較器( sell_object_equal
)。
我們需要特別注意的行是:
return object->hash();
return (*lhs <=> *rhs) == 0;
在他們身上:
return object->hash();
這是散列的委托。 我們不是作為觀察者並試圖為從sell_object
派生的每個可能的類型實現不同的散列,而是要求這些對象自己提供足夠的散列。 在原始問題中, std::hash
專業化是所說的“觀察者”。 它當然不能作為解決方案進行擴展。
為了實現上述目的,我們修改了基礎 class 以施加列出的要求:
struct sell_object {
virtual auto hash() const -> std::size_t = 0;
};
因此我們還需要改變我們的book
和table
類:
class book : public sell_object {
std::string title;
public:
book(std::string title) : title(std::move(title)) { }
auto hash() const -> std::size_t override {
return std::hash<std::string>()(title);
}
};
class table : public sell_object {
int number_of_legs = 0;
public:
table(int number_of_legs) : number_of_legs(number_of_legs) { }
auto hash() const -> std::size_t override {
return std::hash<int>()(number_of_legs);
}
};
return (*lhs <=> *rhs) == 0;
這是一個 C++20 特性,稱為三向比較運算符,有時也稱為宇宙飛船運算符。 我選擇使用它,因為從 C++20 開始,大多數希望具有可比性的類型都將使用此運算符。 這意味着我們還需要具體的類來實現它。 更重要的是,我們需要能夠使用基本引用 ( sell_object&
) 來調用它。 另一個virtual
function(實際上是operator
)需要添加到基礎 class 中:
struct sell_object {
virtual auto hash() const -> std::size_t = 0;
virtual auto operator<=>(sell_object const&) const -> std::partial_ordering = 0;
};
sell_object
的每個子類都需要與其他sell_object
進行比較。 主要原因是我們需要比較我們storage
sell_object
中的sell_object。 為了完整起見,我使用了std::partial_ordering
,因為我們要求每個sell_object
都可以與其他所有sell_object
進行比較。 雖然比較兩個book
或兩個table
會產生強排序(兩個等效對象無法區分的總排序),但我們還 - 通過設計 - 需要支持將book
與table
進行比較。 這有點毫無意義(總是返回false
)。 幸運的是,C++20 在std::partial_ordering::unordered
方面幫助了我們。 這些元素不相等,它們都不大於或小於另一個。 非常適合此類場景。
我們的具體類需要相應地改變:
class book : public sell_object {
std::string title;
public:
book(std::string title) : title(std::move(title)) { }
auto hash() const -> std::size_t override {
return std::hash<std::string>()(title);
}
auto operator<=>(book const& other) const {
return title <=> other.title;
};
auto operator<=>(sell_object const& other) const -> std::partial_ordering override {
if (auto book_ptr = dynamic_cast<book const*>(&other)) {
return *this <=> *book_ptr;
} else {
return std::partial_ordering::unordered;
}
}
};
class table : public sell_object {
int number_of_legs = 0;
public:
table(int number_of_legs) : number_of_legs(number_of_legs) { }
auto hash() const -> std::size_t override {
return std::hash<int>()(number_of_legs);
}
auto operator<=>(table const& other) const {
return number_of_legs <=> other.number_of_legs;
};
auto operator<=>(sell_object const& other) const -> std::partial_ordering override {
if (auto table_ptr = dynamic_cast<table const*>(&other)) {
return *this <=> *table_ptr;
} else {
return std::partial_ordering::unordered;
}
}
};
由於基類的要求,需要override
n operator<=>
。 它們非常簡單 - 如果other
object(我們正在比較這個object 的那個)是相同的類型,我們委托給使用具體類型的<=>
版本。 如果不是,我們有一個類型不匹配,我們報告unordered
排序。
對於那些好奇為什么比較兩個相同類型的<=>
實現不是= default
ed 的人:它將首先使用基類比較,這將委托給sell_object
版本。 這將再次dynamic_cast
並委托給默認實現。 這將比較基礎 class 和...導致無限遞歸。
add()
和remove()
實現一切看起來都很棒,所以我們可以繼續在我們的商店中添加和刪除商品。 然而,我們立即做出了艱難的設計決定。 arguments 應該add()
和remove()
接受什么?
std::unique_ptr<sell_object>
? 這將使他們的實現變得微不足道,但它需要用戶構造一個可能無用的、動態分配的object來調用 function。
sell_object const&
? 這似乎是正確的,但它有兩個問題:1)我們仍然需要構造一個帶有傳遞參數副本的std::unique_ptr
以找到要刪除的適當元素; 2)我們將無法正確實現add()
,因為我們需要具體類型來構造一個實際的std::unique_ptr
以放入我們的 map 。
讓我們使用第二個選項 go 並解決第一個問題。 我們當然不想構建一個無用且昂貴的 object只是為了在存儲 map 中尋找它。 理想情況下,我們希望找到與傳遞的 object 匹配的密鑰 ( std::unique_ptr<sell_object>
)。 幸運的是,透明的哈希器和比較器來拯救。
通過為哈希器和比較器提供額外的重載(並提供public
is_transparent
別名),我們允許查找等效的鍵,而無需匹配類型:
struct sell_object_hash {
auto operator()(std::unique_ptr<sell_object> const& object) const -> std::size_t {
return object->hash();
}
auto operator()(sell_object const& object) const -> std::size_t {
return object.hash();
}
using is_transparent = void;
};
struct sell_object_equal {
auto operator()(
std::unique_ptr<sell_object> const& lhs,
std::unique_ptr<sell_object> const& rhs
) const -> bool {
return (*lhs <=> *rhs) == 0;
}
auto operator()(
sell_object const& lhs,
std::unique_ptr<sell_object> const& rhs
) const -> bool {
return (lhs <=> *rhs) == 0;
}
auto operator()(
std::unique_ptr<sell_object> const& lhs,
sell_object const& rhs
) const -> bool {
return (*lhs <=> rhs) == 0;
}
using is_transparent = void;
};
多虧了這一點,我們現在可以像這樣實現shop::remove()
:
auto remove(sell_object const& to_remove) -> void {
if (auto it = storage.find(to_remove); it != storage.end()) {
it->second--;
if (it->second == 0) {
storage.erase(it);
}
}
}
由於我們的比較器和散列器是透明的,我們可以find()
一個與參數等效的元素。 如果我們找到它,我們會減少相應的計數。 如果它達到0
,我們將完全刪除該條目。
太好了,進入第二個問題。 讓我們列出shop::add()
的要求:
std::unique_ptr
)。sell_object
派生該類型。 我們可以使用 constrained* template
來實現這兩者:
template <std::derived_from<sell_object> T>
auto add(T const& to_add) -> void {
if (auto it = storage.find(to_add); it != storage.end()) {
it->second++;
} else {
storage[std::make_unique<T>(to_add)] = 1;
}
}
再次,這很簡單
只有一件事將我們與正確的實現區分開來。 事實上,如果我們有一個指向用於釋放它的基本 class 的指針(智能或非智能),則析構函數需要是 virtual 。
這將我們引向了sell_object
class 的最終版本:
struct sell_object {
virtual auto hash() const -> std::size_t = 0;
virtual auto operator<=>(sell_object const&) const -> std::partial_ordering = 0;
virtual ~sell_object() = default;
};
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.