繁体   English   中英

使用多态 class 的 std::unique_ptr 作为 std::unordered_map 中的键

[英]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。 booktable继承自 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。 booktable继承自 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 层次结构

首先,我们需要一个基础 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) { }
};

为简单起见(但仍然有一些区别),我选择让它们只有一个不同的字段( titlenumber_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;
};

因此我们还需要改变我们的booktable类:

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会产生强排序(两个等效对象无法区分的总排序),但我们还 - 通过设计 - 需要支持将booktable进行比较。 这有点毫无意义(总是返回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()的要求:

  • 我们需要 object 的具体类型(仅仅引用基础 class 是不够的,因为我们需要创建匹配的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;
    }
}

再次,这很简单

*参考: {1} {2}

正确的破坏语义

只有一件事将我们与正确的实现区分开来。 事实上,如果我们有一个指向用于释放它的基本 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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM