【问题标题】:Smart or raw pointers when implementing data structures?实现数据结构时是智能指针还是原始指针?
【发布时间】:2016-04-12 03:55:20
【问题描述】:

我一直在阅读和试验标准库的智能指针 unique_ptrshared_ptr 虽然它们显然是很多原始指针可能被视为危险的情况的绝佳替代品,但我不确定它们在实现时的用途数据结构。

为了进行实验,我编写了一个使用 shared_ptr 的哈希映射示例 - 根据 Meyer 的 Effective Modern C++,它的大小大约是 unique_ptr 的两倍。出于这个原因,我想使用 unique_ptr,但由于我在 Add 函数中执行的操作、更新和复制,我有点难过。

有人对这个问题有什么建议吗?是否应该使用原始指针来编写数据结构?

#pragma once
#include "core.h"

const int TABLE_SIZE = 256;

template<typename K>
class HashKey {
public:
    unsigned long operator()(const K& p_key) const {
        return (p_key) % TABLE_SIZE;
    }
};

template<typename K, typename T>
class HashNode {
public:
    K m_key;
    T m_value;
    std::shared_ptr<HashNode> next = nullptr;
};


template<typename K, typename T, typename F = HashKey<K>>
class HashMap {
public:
    std::array< std::shared_ptr< HashNode<K, T> >, 128 > m_table;
    F m_hash_function;
    int m_elem_count{ 0 };

    void Add(K p_key, T p_value);
};

template<typename K, typename T, typename F = HashKey<K>>
void HashMap<K, T, F>::Add(K p_key, T p_value)
{
    unsigned long key = m_hash_function(p_key);

    std::shared_ptr<HashNode<K, T>> new_node = std::make_shared<HashNode<K, T>>();
    new_node->m_key = p_key;
    new_node->m_value = p_value;


    if (m_table[key] == nullptr) {
        /* only item in the bucket */
        m_table[key] = std::move(new_node);
        m_elem_count++;
    }
    else {
        /* check if item exists so it is replaced */
        std::shared_ptr< HashNode<K, T> > current = m_table[key];
        std::shared_ptr< HashNode<K, T> > previous = m_table[key];
        while (current != nullptr && p_key != current->m_key ) {
            previous = current;
            current = current->next;
        }
        if (current == nullptr) {
            previous->next = new_node;
            //current = new_node;
            m_elem_count++;
        }
        else {
            current->m_value = p_value;
        }

    }
}

void TestHashMap() {

    HashMap<int, std::string> hash_map;

    hash_map.Add(1, "one");
    hash_map.Add(2, "does");
    hash_map.Add(3, "not");
    hash_map.Add(50, "simply");
    hash_map.Add(11, "waltz");
    hash_map.Add(11, "into");
    hash_map.Add(191, "mordor");

    std::cout << hash_map.m_elem_count << std::endl;
}

【问题讨论】:

  • 一般来说,您应该从所有权的角度来考虑新的智能指针,而不是简单地自动删除指针。一个“资源”可以同时拥有多个所有者 (std::shared_ptr) 还是一次只有一个所有者 (std::unique_ptr)。
  • 我不知道你为什么觉得这里需要指针——或者std::array,就此而言。为什么不按照std::vector&lt;HashNode&lt;K, T&gt;&gt; 来实现哈希映射?一般来说,这个问题让我有点困惑:你应该做的大部分事情是编写数据结构(以及它们的算法)。因此,对于数据结构,您当然会使用智能指针,而不是哑指针——那么,您还会将智能指针用于什么用途呢?没有别的了。
  • 在树形数据结构中编写诸如旋转之类的代码时,我倾向于进行微优化,这通常包括在他们放弃概念所有权后的几条指令中使用指针。只要我使用原始指针并且我理解代码的所有权语义,我就可以在安全有效的地方准确地改变所有权规则。使用智能指针,优化器永远无法将生成的代码恢复到我使用原始指针编写的代码的效率。不太熟练的程序员会发现获得收益的机会更少,并且冒更多错误的风险。
  • @JSF 我觉得那是个谬论。当然,在某些情况下您可以进行优化(例如,std::unique_ptr 目前不能传递到寄存器内的函数中,而原始指针可以)。但总的来说,很难做出这样的断言,因为编译器中的优化器变得越来越好,而且你在手动实现中做出的许多假设会因平台而异。特别是,我什至不确定您所说的“在 [放弃] 概念所有权之后使用……”是什么意思。这听起来像 UB。
  • 独特的“概念”所有权意味着一次一个指针“拥有”该对象,程序员知道哪一个而编译器不知道。在移交概念所有权之后,您仍然有一个指向有效对象的指针,直到/除非新所有者将其删除。因此,在所有权被移交的那一刻和新所有者可以删除对象的稍后时间之间,原始指针可以有效访问该对象,而 std::unique_ptr 则不会(所有假设都不需要异常安全)部分代码)。

标签: c++ pointers data-structures


【解决方案1】:

智能指针的选择取决于你的数据结构如何“拥有”堆分配的对象

如果您需要简单地观察,而不是拥有一个对象(独立于它是否是堆分配的),一个原始 指针、引用std::reference_wrapper是合适的选择。

如果您需要唯一所有权(最多只有一个堆分配对象的所有者),请使用std::unique_ptr。它没有额外的时间/内存开销。

如果您需要共享所有权(堆分配对象的任意数量的所有者),请使用std::shared_ptr。它会导致额外的时间/内存开销,因为必须存储额外的指针(指向引用计数元数据),并且保证访问它是线程安全的。

没有必要使用std::unique_ptr(代替原始指针),除非您确实需要拥有该对象。

假设您需要拥有该对象,则无需使用std::shared_ptr(代替std::unique_ptr,除非您确实需要共享所有权语义


在您的情况下,您的HashMap 中似乎有最多的堆节点。因此,我假设您希望 HashMap 实例成为节点的唯一所有者

你应该使用什么类型?

template<typename K, typename T, typename F = HashKey<K>>
class HashMap {
public:
    std::array</* ? */, 128 > m_table;
    // ...
};

你有两个选择:

  1. 如果您想以堆间接方式存储对象,请使用std::unique_ptr,因为这些堆分配对象的唯一所有者是并且将永远是HashMap 实例.

  2. 如果你想将对象直接存储到HashMap 中,而不使用堆间接,那么根本不要使用任何指针。这可能会导致非常大的 HashMap 实例。访问next节点的接口变得繁琐。


选项 1(在堆中存储节点):

这是最常见的,也可能是最好的选择。

template<typename K, typename T, typename F = HashKey<K>>
class HashMap {
public:
    std::array<std::unique_ptr<HashNode<K, T>>, 128 > m_table;
    // ...
};

这将导致更轻的 (就内存占用而言) HashMap 实例。

注意:使用std::vector 代替std::array 会显着减小HashMap 的大小,但会引入额外的堆间接寻址。 这是实现类似数据结构的常用方法。您通常希望HashMap 实例尽可能轻量级,以便可以有效地复制/移动/存储它。

没有必要使用智能指针来连接彼此之间的节点,因为这些节点由HashMap 独占拥有。 原始指针就足够了

template<typename K, typename T>
class HashNode {
public:
    // ...
    HashNode* next_ptr = nullptr;
    auto& next() 
    { 
        assert(next_ptr != nullptr);
        return *next_ptr;
    }
};

上面的代码可以正常工作,假设访问nextHashMap仍然存在。


选项 2(在地图实例中存储节点):

template<typename K, typename T, typename F = HashKey<K>>
class HashMap {
public:
    std::array<HashNode<K, T>, 128 > m_table;
    // ...
};

HashMap 实例可能很大,具体取决于HashNode&lt;K, T&gt; 的大小。

如果您选择将节点直接存储到 HashMap 而没有堆间接,则必须使用索引来访问内部数组,因为移动/复制 HashMap 会改变节点。

template<typename K, typename T>
class HashNode {
public:
    // ...
    int next_index = -1;
    auto& next(HashMap& map) 
    { 
        assert(next_index != -1);
        return map.m_table[next_index]; 
    }
};

【讨论】:

  • 选项 1 正是我想到的替代方案,我只是不确定这是否是正确的方法,非常感谢!您愿意详细说明堆间接这个术语吗?
  • @dgouder:这不是一个“官方”术语——我用它来描述当您通过指针访问在堆上分配的对象时发生的间接寻址。 SomeClass x; x.some_method(); std::unique_ptr<SomeClass>(x); x->some_method(); x 指向的对象。这可能对缓存不友好,因为您必须在内存中跳转。
猜你喜欢
  • 2015-01-24
  • 2020-11-18
  • 1970-01-01
  • 2016-08-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-12-03
相关资源
最近更新 更多