【问题标题】:Should I use a map or a set if my key is part of my value?如果我的键是我的价值的一部分,我应该使用地图还是集合?
【发布时间】:2012-10-05 23:42:31
【问题描述】:

在 C++ 中,我有一个按其名称排序的类,即 std::string。我希望std::mapstd::set 中的每个唯一名称只有一个。

我可以使用std::set,因为operator< 将按其名称对我的实例进行排序,但是,我需要按其名称查找实例。使用键是名称的映射是直截了当的,但是,我也可以使用一个集合并构造一个具有我希望查找的名称的类的虚拟实例,以在集合中定位给定类的实际实例名字。

我想我应该只使用地图以使代码直截了当,但想知道是否有办法使用该集合,因为密钥实际上是我的对象的一部分,因此避免了一些冗余。

有没有办法使用集合并能够以干净的方式通过键定位对象,或者我应该只使用地图并完成它?

这是要插入的类(草稿形式),在每个目录中都有一组节点或节点映射,以节点名称为键:

class Node {
public:
  Node(Directory &parent, const std::string &name)
    : _name(name),
      _parent(&parent),
      _isRoot(false) {
    if (name.empty()) {
      throw InvalidNodeNameError(name);
    }
  }

protected:
  // This is only used for the root directory:
  Node()
    : _name(""),
      _parent(0),
      _isRoot(true) {
  }
  Node(const std::string &name)
    : _name(name),
      _parent(0),
      isRoot(false) {
  }

public:
  virtual ~Node() {
    if (parent()) {
      parent()->remove(*this);
    }
  }

  bool operator<(const Node &rhs) const {
    return _name < rhs._name;
  }

  Directory *parent() const {
    return _parent;
  }
  void setParent(Directory *parent) {
    _parent = parent;
  }

  const std::string &name() const {
    return _name;
  }

  bool isRoot() const {
    return _isRoot;
  }

  std::string pathname() const {
    std::ostringstream path;

    if (parent()) {
      path << parent()->pathname() << '/';
    } else {
      path << '/';
    }
    path << name();

    return path.str();
  }

private:
  // Not defined:
  Node(const Node &rhs);
  Node &operator=(const Node &rhs);

private:
  std::string  _name;
  Directory   *_parent;
  const bool   _isRoot;

};

【问题讨论】:

  • I asked a very similar question before。实际上,所有的答案都很好。唯一的区别是你的涉及比较,而我的涉及散列 - mapunordered_map
  • 看看你的课可能会有所帮助。
  • 我认为 set 通常是作为 map 的一个特例来实现的,所以你可能不会为自己买太多的性能。一种方法可能是根据地图来实现,但提供一个界面,该界面被限制为更像“集合”,只是远不及集合本身。 IMO,这比反过来尝试包装设置要容易。

标签: c++ key stdmap stdset


【解决方案1】:

实际上,您可以只使用 map<:string node>,但要花费一个额外的指针,但我想您可能知道这一点,并且需要花一些时间才能得到您想要的。

我一直认为 std::set 没有明确的 KeyExtractor 模板参数是一件很痛苦的事情,特别是因为我看到的每个实现都使用其中一个实现不重复代码在(多)地图和(多)集之间。这是一个快速而肮脏的 hack,尚未完成,它公开了 GNU 标准 C++ 库的一些机制,以创建“keyed_set”容器:

// Deriving from the tree is probably not a good idea, but it was easy.

template<typename Key, typename Val, typename Extract,
         typename Compare = std::less<Key>, typename Alloc = std::allocator<Val>>
class keyed_set : public std::_Rb_tree<Key, Val, Extract, Compare, Alloc> {
  using Base = std::_Rb_tree<Key, Val, Extract, Compare, Alloc>;

  public:
    template<typename ...Args>
    auto insert(Args... args)
         ->decltype(Base()._M_insert_unique(std::declval<Args>()...)) {
      return this->_M_insert_unique(args...);
    }

    typename Base::iterator insert(typename Base::const_iterator i,
                                   const Val& val) {
      return this->_M_insert_unique_(i, val);
    }

    Val& operator[](const Key& key) {
      auto i = this->lower_bound(key);
      if (i == this->end() || this->key_comp()(key, Extract()(*i))) {
        i = this->_M_insert_unique_(i, Val(key));
      }
      return *i;
    }
};

要完成这项工作,您需要提供一个密钥提取器,如下所示:

template<class T>
struct KeyExtractor;

template<>
struct KeyExtractor<Node> {
  const std::string& operator()(const Node& n) { return n.name(); }
};

要让我的 operator[] 版本正常工作,您需要值类型有一个构造函数,该构造函数将其键类型作为参数。

我遗漏了很多东西(例如擦除);但是做一个简单的测试就足够了。

从 KeyExtractor 的返回类型中默认键类型可能会更好,但这会涉及以不同的顺序放置模板参数,而且我已经浪费了太多时间没有注意到 _M_insert_unique 和 _M_insert_unique_ 是拼写不同(大概是为了避免模板实例化问题。)

这是我用来检查以确保其有效的示例; MyKeyedClass 有一个名称,带有一个字符串向量,以及一个与每个字符串相关联的双精度值。 (没有崇高的目的。)

int main(void) {
  keyed_set<std::string, MyKeyedClass, KeyExtractor<MyKeyedClass>> repo;
  for (std::string name, val; std::cin >> name >> val; ) {
    try {
      size_t end;
      double d = std::stod(val, &end);
      if (end != val.size())
        throw std::invalid_argument("trailing letters");
      repo[name].increment(d);
    } catch (std::invalid_argument(e)) {
      repo[name].push(val);
    } catch (std::out_of_range(e)) {
      std::cerr << "You managed to type an out of range double" << std::endl;
    }
  }
  std::for_each(repo.begin(), repo.end(),
                [](MyKeyedClass& c){ std::cout << c << std::endl; });
  return 0;
}

【讨论】:

    【解决方案2】:

    我认为因为Node 需要在构造期间引用Directory,所以创建一个虚拟节点来按名称搜索您的集合将使Node 类更加混乱。

    要使用set,您可能需要在某处创建一个静态Directory,并将其用作新虚拟构造函数Node(const std::string&amp;) 中的虚拟引用。如果您没有声明 explicit,您可以在调用 set::find 时直接使用 string

    您可以改为将类转换为使用指针...但这会改变其内部语义:Directory&amp; 始终有效,而 Directory* 不必如此。问问自己,是否仅仅因为您偏爱 set 容器,就想让读者不太清楚语义。

    所以我的观点在这种情况下很清楚......你有一个选择:要么使用map 并保持你的班级干净,要么使用set 并编写一些无用的支持垃圾代码别的。 =)

    【讨论】:

      【解决方案3】:

      我通过 key 的代理来实现这些类,例如在 std::string 的情况下,我有一个名为轻量级字符串的类,它实现 operator &lt; 并且在内部它指向 std::string 然后我使用 map 和既具有使用 map 的简单性,又具有没有 2 个版本的 key 的性能。

      【讨论】:

        【解决方案4】:

        对于您的情况,请检查您的编译器是否足够老,仍然可以使用 COW(写入时复制)策略实现 std::string。这在 C++11 中发生了变化,但旧的编译器版本仍然是 COW... 这具有的优点是,将字符串作为键和作为值的一部分的映射几乎不会花费您任何成本。但请注意,这将在未来改变(或已经改变)......

        【讨论】:

          猜你喜欢
          • 2010-11-03
          • 1970-01-01
          • 2015-01-07
          • 2014-08-31
          • 1970-01-01
          • 2021-01-02
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多