【问题标题】:Avoiding copies when using structs in STL sets and custom comparison function在 STL 集中使用结构和自定义比较函数时避免复制
【发布时间】:2012-11-27 10:35:06
【问题描述】:

假设您有一个包含一堆成员的结构,并且您想使用通过其中一个成员引用的特定值作为集合中的键,如下所示:

class ComplexClass {
 public:
  const string& name() const;
  // tons of other stuff
};
struct MyStruct {
  ComplexClass* c;
  MoreStuff* x;
};
struct CmpMyStruct {
  bool operator()(const MyStruct& lhs, const MyStruct& rhs) {
    return lhs.c->name() < rhs.c->name();
  }
};
typedef set<MyStruct, CmpMyStruct> MySet;
MySet my_set;

这很好用,但是现在我想通过 字符串名称 进行查找,但 my_set.find() 现在当然需要一个 'const MyStruct&'。如果名称不是从 ComplexClass 中取出,而是 MyStruct 的成员,我可以快速伪造 MyStruct 的一个实例并使用它:

MyStruct tmp_for_lookup;
tmp_for_lookup.name = "name_to_search";  // Doesn't work of course
MySet::iterator iter =  my_set.find(tmp_for_lookup);

但是,如上所述,它不是这样工作的,名称在 ComplexClass 中,所以我必须至少在其中放一个模拟或其他东西。

所以我真正想要的是 STL 集不会比较 MyStructs,而是首先从 MyStruct(具有字符串类型)中“投影”键,然后执行其操作,包括 find(),在那。 我开始深入研究 gcc 中 set/map 的实现,看看他们是如何为地图解决这个问题的,很遗憾地看到他们实际上在内部 _Rb_tree 中解决了它,但没有公开它,因为它不是标准。来自 gcc 的 stl_tree.h:

template<typename _Key, typename _Val, typename _KeyOfValue,
         typename _Compare, typename _Alloc = allocator<_Val> >
  class _Rb_tree
  {
....
template<typename _Key, typename _Val, typename _KeyOfValue,
         typename _Compare, typename _Alloc>
  typename _Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::iterator
  _Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::
  find(const _Key& __k)

然后在stl_map.h中:

    typedef _Rb_tree<key_type, value_type, _Select1st<value_type>,
             key_compare, _Pair_alloc_type> _Rep_type;

注意它是如何使用 '_Select1st' 将键从 value_type 中投射出来的,所以 find() 实际上可以只使用键。另一方面,在这种情况下,stl_set.h 只使用 Identity,正如预期的那样。

所以我想知道,有没有一种方法是我目前缺少的,因为我可以使用普通的 STL 集/地图实现相同的美感和效率(即我绝对不想使用 GCC 特定的 _Rb_tree直接),这样我真的可以做

MySet::iterator iter = my_set.find("somestring");

请注意,我特别不想将 my_set 更改为从字符串到 MyStructs 的映射,即我不想将字符串(或对它的引用)从 ComplexClass 中复制出来,这样我就可以做到 @987654326 @ 或 map&lt;const string&amp;, MyStruct&gt;

在这一点上,这几乎更像是一种思维练习,但看起来很有趣:)

【问题讨论】:

    标签: c++ stl set


    【解决方案1】:

    现在我想通过字符串名称进行查找,但 my_set.find() 现在当然需要一个 'const MyStruct&'。

    这是std::set接口的一个众所周知的缺陷。

    boost::multi_indexordered_unique 索引为std::set 提供了额外的find() 函数的接口,这些函数采用可比较的键,而不是整个集合元素。

    #include <boost/multi_index_container.hpp>
    #include <boost/multi_index/ordered_index.hpp>
    
    struct ComplexClass {
        std::string name() const;
    
        struct KeyName {
            typedef std::string result_type;
            std::string const& operator()(ComplexClass const& c) const {
                return c.name();
            }
        };
    };
    
    namespace mi = boost::multi_index;
    
    typedef mi::multi_index_container<
          ComplexClass
        , mi::indexed_by<
                mi::ordered_unique<ComplexClass::KeyName>
              >
        > ComplexClassSet;
    
    int main() {
        ComplexClassSet s;
        // fill the set
        // ...
        // now search by name
        ComplexClassSet::iterator found = s.find("abc");
        if(found != s.end()) {
            // found an element whose name() == "abc"
        }
    }
    

    更多详情请见http://www.boost.org/doc/libs/1_52_0/libs/multi_index/doc/tutorial/key_extraction.html

    【讨论】:

    • 很好,谢谢!这绝对看起来会起作用,但我更愿意避免提升并在我的特定情况下坚持使用 STL。尽管您声称这是 set-interface 的一个众所周知的缺陷这一事实似乎表明除了基本上重新实现 set 之外没有解决方案:-(
    • @TKL 我希望他们能在 C++11 中扩展 std::set 的接口,使用嵌入在 boost::multi_index 等元素中的按键查找功能,但这并没有发生。因此,我一直使用boost::multi_index,而std::set 就足够了。
    • @Maxim,我已经向委员会提交了一篇论文来解决这个问题:open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3465.pdf。让我们看看对提案的接受情况。
    • @JoaquínMLópezMuñoz 如果它被接受到 C++ 的下一个版本中会很棒。
    【解决方案2】:

    如果您可以处理比较虚拟调用的开销,您可以使用这样的技巧:

    class ComplexClass {
     public:
      const string& name() const;
      // tons of other stuff
    };
    struct MyStruct {
      ComplexClass* c;
      MoreStuff* x;
      virtual const string& key() const { return c->name(); } /* change */
    };
    struct CmpMyStruct {
      bool operator()(const MyStruct& lhs, const MyStruct& rhs) {
        return lhs.key() < rhs.key(); /* change */
      }
    };
    typedef set<MyStruct, CmpMyStruct> MySet;
    MySet my_set;
    

    然后,为了查找,添加以下结构:

    struct MyLookupStruct : MyStruct {
      string search_key;
      explicit MyLookupStruct(const string& key) : search_key(key) {}
      virtual const string& key() const { return search_key; }
    };
    /* .... */
    MySet::iterator iter =  my_set.find(MyLookupStruct("name to find"));
    

    这取决于std::set&lt;&gt;::find 不复制参数,这似乎是一个合理的假设(但据我所知,没有明确保证)。

    【讨论】:

      【解决方案3】:

      AFAIK,只有 C++ 标准库的 std::set 没有办法,但您可以将 std::find_if 与谓词一起使用

      struct cmp_for_lookup {
          std::string search_for;
          cmp_for_lookup(const std::string &s) : search_for(s) {}
          bool operator()(const MyStruct &elem) { return elem.c->name() == search_for; }
      };
      
      std::find_if(my_set.begin(), my_set.end(), cmp_for_lookup("name_to_search"));
      

      【讨论】:

      • 我认为这会降低地图的效率增益并导致线性扫描,所以我不希望这样,但是谢谢。
      • -1,std::find_if 是线性搜索,用std::set 有什么意义呢?
      • @aleguna set 还有其他属性。无论如何,我期待您的解决方案,如果有的话。
      【解决方案4】:

      显然,由于 boost 是不可能的,而且std::set 并不打算提供多个索引,因此您必须构建自己的额外索引。

      std::string name_of_struct(const MyStruct* s){
         return s->c->name();
      }
      
      
      ...
      std::map<std::string, MyStruct*> indexByName;
      std::transform( my_set.begin(), my_set.end(), 
                      std::inserter(indexByName,indexByName.begin()),
                      &name_of_struct );
      
       ...
       MyStruct* theFoundStruct=indexByName("theKey");
      

      (注意:这个想法可行,但编译器仍然会抱怨,因为这是我的头等大事)。

      【讨论】:

      • 是的,但在这一点上,其他地图不再有真正的理由,而且这比我这样做更糟糕:map&lt;const string&amp;, MyStruct&gt;,每当我插入 id-reference添加MyStruct
      【解决方案5】:

      请记住,关联容器(setmap)中的键是 const,因为它们在插入时已排序,以后无法重新排序。因此,任何实际引用插入对象的解决方案都容易受到您在插入后更改密钥成员的可能性的影响。

      所以,最通用的解决方案是使用map 并复制密钥成员。

      如果键不会改变,那么您可以使用指针作为键。使用临时文件进行查找需要获取指向临时文件的指针,但这没关系,因为 findoperator[] 不会保留其参数的副本。

      #include <map>
      #include <string>
      
      /* Comparison functor for maps that don't own keys. */
      struct referent_compare {
          template< typename t >
          bool operator () ( t *lhs, t *rhs )
              { return * lhs < * rhs; }
      };
      
      /* Convenience function to get a const pointer to a temporary. */
      template< typename t >
      t const *temp_ptr( t const &o ) { return &o; }
      
      struct s {
          std::string name;
          long birthdate;
          double score;
      };
      
      /* Maps that don't own keys.
         Generalizing this to a template adapter left as an exercise. */
      std::map< std::string const *, s, referent_compare > byname;
      std::map< double const *, s, referent_compare > byscore;
      
      int main() {
          s bob = { "bob", 12000000, 96.3 };
          byname.insert( std::make_pair( & bob.name, bob ) );
          byscore.insert( std::make_pair( & bob.score, bob ) );
      
          byname[ temp_ptr< std::string >( "bob" ) ].score = 33.1;
      }
      

      当然,另一种解决方案是使用set 的指针并定义比较函数来访问该成员。指向成员的指针可用于概括这一点,因此关键成员只是比较函子的模板参数。

      http://ideone.com/GB2jfn

      【讨论】:

      • 是的,切换到地图并复制密钥或指向它的指针/引用并将其传递给您展示的分拣机是明显的解决方案 - 我希望找到一种方法来避免不得不做任何复制(或重构数据结构)只是为了规避 STL 集的 api 的缺陷。请参阅 Maxim 帖子上的 cmets,听起来这是 STL 集的一个已知问题,所以我想我不走运 :-( 感谢您的帖子。
      • @TKL 这个解决方案重构数据结构以避免复制。 temp_ptr 解决了 set/map 的问题,即排序 operator&lt; 必须在左侧和右侧采用相同的类型,方法是使用指向成员的指针将临时调整为通用类型。只需添加一个带有指向成员的接口,您将获得一个低开销、低样板的解决方案。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-07-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-10-14
      相关资源
      最近更新 更多