这个问题的逻辑存在一些问题。首先:
考虑一个具有多态性的类层次结构,其中sell_obj
是基类,book
和table
继承自该类。我们现在知道我们需要创建一个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
,我们应该为所有三个类指定哈希函数。为了简化事情,我在主函数中像这样指定了它们:[...]
这也是一种不太理想的方法。这将要求每次在类层次结构中添加另一个子类时都要更改代码的那部分。最好的方法是通过委托方式实现多态地进行哈希(和比较),以避免此类问题,正如@1201programalarm所建议的那样。
[...] implementation of two, crucial functions:
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
引用接受参数表明修改对象。其次,从使用&
在参数上获得的指针创建n_ptr
是极其危险的。它假设对象在堆上分配并且未拥有。这种情况通常不会发生,而且非常危险。如果传递的对象在堆栈上或已由其他所有者管理,则这是灾难的配方(如段错误)。
更重要的是,它几乎肯定会导致灾难,因为两个add_sell_obj()
和remove_sell_obj()
都创建了std::unique_ptr
指向可能相同的对象。这正是原始问题中main()
的情况。两个指向同一对象的std::unique_ptr
会导致double delete
。
虽然使用C++(相对于Java)并不一定是解决此问题的最佳方法,但有一些有趣的工具可用于此任务。以下代码假定使用C++20。
类层次结构
首先,我们需要一个基类,用于引用存储在商店中的所有对象:
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
类将代表任何sell_object
的存储,需要以某种方式存储任何sell_object
。为此,我们需要使用指向基类的指针或引用。你不能拥有引用的容器,所以最好使用指针。智能指针。
最初的问题建议使用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
作为我们的映射键。这意味着存储将复制传递的对象并使用它所拥有的副本与我们查询(添加或删除)的元素进行比较。但不会复制超过一个相等的对象。
存储的修复版本
然而,存在一个问题。 std::unordered_map
使用哈希,我们需要为std::unique_ptr<sell_object>
提供一个哈希策略。好吧,已经有一个并且它使用T*
的哈希策略。问题是我们想要自定义哈希。因此,我选择了与问题中提出的方法不同的方法。我将选择一个自定义哈希对象和一个自定义比较器:
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
专门化就是所谓的“观察者”。它显然不是一个可扩展的解决方案。
为了实现上述目标,我们修改基类以强制执行所列要求:
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
函数(实际上是operator
):
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`。为了完整起见,我使用了
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
的 operator<=>
是由于基类的要求而必需的。它们非常简单 - 如果other
对象(我们正在将this对象与之进行比较的对象)是相同类型的,则委托给使用具体类型的 <=>
版本。如果不是,我们就有了类型不匹配并报告 unordered
排序。
对于那些好奇为什么比较两个相同类型的 <=>
实现不是 = default
的人:它会首先使用基类比较,然后委托给 sell_object
版本。这会再次进行 dynamic_cast
并委托给默认实现。这将比较基类并... 导致无限递归。
add()
和 remove()
的实现
一切看起来都很棒,所以我们可以开始添加和删除商品到我们的商店。但是,我们立即遇到一个难以决定的设计问题。 add()
和 remove()
应该接受哪些参数?
std::unique_ptr<sell_object>
?这将使它们的实现变得简单,但需要用户构造一个可能无用的动态分配对象仅为了调用函数。
sell_object const&
?这似乎是正确的,但有两个问题:1)我们仍然需要使用传递参数的副本构造一个std::unique_ptr
来查找要删除的适当元素;2)我们将无法正确实现add()
,因为我们需要具体类型来构造实际的std::unique_ptr
放入我们的映射中。
让我们选择第二个选项并解决第一个问题。我们肯定不想构造一个无用且昂贵的对象仅为了在存储映射中查找它。理想情况下,我们希望找到与传递对象匹配的键(std::unique_ptr<sell_object>
)。幸运的是,透明哈希和比较器来拯救我们了。
通过为哈希器和比较器提供额外的重载(并提供一个公共的 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
。
我们可以通过一个受限制的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;
}
}
这实际上相当简单
*参考资料:{1} {2}
正确的销毁语义
我们离正确的实现只有一步之遥。那就是,如果我们有一个指向基类的指针(智能或非智能),用于释放它,则析构函数需要是虚拟的。
这将带领我们进入sell_object
类的最终版本:
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;
};
请查看具有示例和附加打印实用程序的完整实现。
s_o
now.”当s_o
是同一个对象时,或者如果其中一个被两次调用,它们就不是非常独特的。考虑到你的函数无法判断它是否唯一,你应该重新思考API设计。 - chrisshared_ptr
而不是unique_ptr
。 - 1201ProgramAlarmsell_obj
中实现hash
作为虚函数(在派生类中重写),以消除那个dynamic_cast
。 - 1201ProgramAlarm