【问题标题】:std::map default valuestd::map 默认值
【发布时间】:2011-01-20 23:33:41
【问题描述】:

当key不存在时,有没有办法指定std::map的operator[]返回的默认值?

【问题讨论】:

    标签: c++ stdmap


    【解决方案1】:

    使用 C++20 编写这样的 getter 很简单:

    constexpr auto &getOrDefault(const auto &map, const auto &key, const auto &defaultValue)
    {
        const auto itr = map.find(key);
        return itr == map.cend() ? defaultValue : itr->second;
    }
    

    【讨论】:

      【解决方案2】:

      如果您想继续使用operator[],就像您不必指定除T()(其中T 是值类型)以外的默认值时一样,您可以继承@ 987654325@ 并在构造函数中指定不同的默认值:

      #include <iostream>
      #include <map>
      #include <string>
      
      int main() {
        class string_with_my_default : public std::string {
        public:
          string_with_my_default() : std::string("my default") {}
        };
      
        std::map<std::string, string_with_my_default> m;
      
        std::cout << m["first-key"] << std::endl;
      }
      

      但是,如果 T 是原始类型,请尝试以下操作:

      #include <iostream>
      #include <map>
      #include <string>
      
      template <int default_val>
      class int_with_my_default {
      private:
        int val = default_val;
      public:
        operator int &() { return val; }
        int* operator &() { return &val; }
      };
      
      int main() {
        std::map<std::string, int_with_my_default<1> > m;
      
        std::cout << m["first-key"] << std::endl;
        ++ m["second-key"];
        std::cout << m["second-key"] << std::endl;
      }
      

      另见C++ Class wrapper around fundamental types

      【讨论】:

        【解决方案3】:

        如果你可以访问C++17,我的解决方案如下:

        std::map<std::string, std::optional<int>> myNullables;
        std::cout << myNullables["empty-key"].value_or(-1) << std::endl;
        

        这允许您在每次使用地图时指定一个“默认值”。这可能不一定是您想要或需要的,但为了完整起见,我会在此处发布。这种解决方案非常适合函数式范式,因为地图(和字典)通常以这种风格使用:

        Map<String, int> myNullables;
        print(myNullables["empty-key"] ?? -1);
        

        【讨论】:

        • 这假定地图本身包含原始选项,但情况并非总是如此。
        • @Zoe 我不确定你的建议是什么。我的解决方案是使用 T 的可选值作为值来创建地图。如果您是从头开始编写代码(或修改现有代码),您可以随时创建原始选项映射(有一些限制,例如没有可选引用)
        • 但如果地图超出你的控制范围,它就不起作用——这就是我的观点。这不是适用于任意地图的通用解决方案
        • @Zoe 啊,现在我明白了。感谢您的意见和澄清。
        【解决方案4】:

        Pre-C++17,使用std::map::insert(),对于较新的版本使用try_emplace()。这可能违反直觉,但这些函数实际上具有 operator[] 的行为以及自定义默认值。

        意识到我参加这个聚会已经很晚了,但是如果您对具有自定义默认值的 operator[] 的行为感兴趣(即:找到具有给定键的元素,如果它不存在 插入选择的默认值并返回对新插入值或现有值的引用),在 C++17 之前已经有一个函数可供您使用:std::map::insert()。如果键已经存在,insert 将不会实际插入,而是将迭代器返回到现有值。

        假设您想要一个字符串到整数的映射,如果键不存在,则插入默认值 42:

        std::map<std::string, int> answers;
        
        int count_answers( const std::string &question)
        {
            auto  &value = answers.insert( {question, 42}).first->second;
            return value++;
        }
        
        int main() {
        
            std::cout << count_answers( "Life, the universe and everything") << '\n';
            std::cout << count_answers( "Life, the universe and everything") << '\n';
            std::cout << count_answers( "Life, the universe and everything") << '\n';
            return 0;
        }
        

        应该输出 42、43 和 44。

        如果构造映射值的成本很高(如果复制/移动键或值类型很昂贵),这会带来显着的性能损失,这可以通过 C++17 的try_emplace() 规避。

        【讨论】:

          【解决方案5】:

          扩展答案https://stackoverflow.com/a/2333816/272642,这个模板函数使用std::map的key_type和mapped_type typedefs来推断key和def的类型。 这不适用于没有这些 typedef 的容器。

          template <typename C>
          typename C::mapped_type getWithDefault(const C& m, const typename C::key_type& key, const typename C::mapped_type& def) {
              typename C::const_iterator it = m.find(key);
              if (it == m.end())
                  return def;
              return it->second;
          }
          

          这让你可以使用

          std::map<std::string, int*> m;
          int* v = getWithDefault(m, "a", NULL);
          

          不需要像std::string("a"), (int*) NULL这样的参数。

          【讨论】:

            【解决方案6】:

            一种解决方法是使用map::at() 而不是[]。 如果键不存在,at 会抛出异常。 更好的是,这也适用于矢量,因此适用于可以将地图与矢量交换的通用编程。

            对未注册的密钥使用自定义值可能很危险,因为该自定义值(如 -1)可能会在代码中进一步处理。除了例外,更容易发现错误。

            【讨论】:

              【解决方案7】:

              C++17 提供了try_emplace 正是这样做的。它为值构造函数接受一个键和一个参数列表,并返回一对:iterator 和 bool。:http://en.cppreference.com/w/cpp/container/map/try_emplace

              【讨论】:

                【解决方案8】:

                虽然这并不能完全回答问题,但我已经用这样的代码规避了这个问题:

                struct IntDefaultedToMinusOne
                {
                    int i = -1;
                };
                
                std::map<std::string, IntDefaultedToMinusOne > mymap;
                

                【讨论】:

                • 这对我来说是最好的解决方案。易于实施、非常灵活且通用。
                • 请注意,您可以添加operator int() const {return i;},在大多数情况下,您可以将其视为int。
                • @acegs 你可以让它真正通用:template &lt;typename T&gt; struct default { T t; }; 然后你可以考虑添加转换运算符和转换构造函数
                【解决方案9】:

                更通用版本,支持C++98/03及更多容器

                适用于通用关联容器,唯一的模板参数是容器类型本身。

                支持的容器:std::map、std::multimap、std::unordered_map、std::unordered_multimap、wxHashMap、QMap、QMultiMap、QHash、QMultiHash等

                template<typename MAP>
                const typename MAP::mapped_type& get_with_default(const MAP& m, 
                                                             const typename MAP::key_type& key, 
                                                             const typename MAP::mapped_type& defval)
                {
                    typename MAP::const_iterator it = m.find(key);
                    if (it == m.end())
                        return defval;
                
                    return it->second;
                }
                

                用法:

                std::map<int, std::string> t;
                t[1] = "one";
                string s = get_with_default(t, 2, "unknown");
                

                这里有一个类似的实现,使用了一个包装类,更类似于Python中dict类型的get()方法:https://github.com/hltj/wxMEdit/blob/master/src/xm/xm_utils.hpp

                template<typename MAP>
                struct map_wrapper
                {
                    typedef typename MAP::key_type K;
                    typedef typename MAP::mapped_type V;
                    typedef typename MAP::const_iterator CIT;
                
                    map_wrapper(const MAP& m) :m_map(m) {}
                
                    const V& get(const K& key, const V& default_val) const
                    {
                        CIT it = m_map.find(key);
                        if (it == m_map.end())
                            return default_val;
                
                        return it->second;
                    }
                private:
                    const MAP& m_map;
                };
                
                template<typename MAP>
                map_wrapper<MAP> wrap_map(const MAP& m)
                {
                    return map_wrapper<MAP>(m);
                }
                

                用法:

                std::map<int, std::string> t;
                t[1] = "one";
                string s = wrap_map(t).get(2, "unknown");
                

                【讨论】:

                • MAP::mapped_type& 返回不安全,因为类型名 MAP::mapped_type& defval 可能超出范围。
                【解决方案10】:

                正如其他答案所说,该值是使用默认构造函数初始化的。然而,补充一点是有用的,在简单类型的情况下(整数类型,如 int、float、pointer 或 POD(计划旧数据)类型),值是零初始化的(或通过值初始化归零(这实际上是相同的东西),取决于使用的 C++ 版本)。

                无论如何,底线是,具有简单类型的映射将自动对新项目进行零初始化。所以在某些情况下,无需担心显式指定默认初始值。

                std::map<int, char*> map;
                typedef char *P;
                char *p = map[123],
                    *p1 = P(); // map uses the same construct inside, causes zero-initialization
                assert(!p && !p1); // both will be 0
                

                有关此事的更多详细信息,请参阅Do the parentheses after the type name make a difference with new?。

                【讨论】:

                  【解决方案11】:

                  不,没有。最简单的解决方案是编写自己的免费模板函数来执行此操作。比如:

                  #include <string>
                  #include <map>
                  using namespace std;
                  
                  template <typename K, typename V>
                  V GetWithDef(const  std::map <K,V> & m, const K & key, const V & defval ) {
                     typename std::map<K,V>::const_iterator it = m.find( key );
                     if ( it == m.end() ) {
                        return defval;
                     }
                     else {
                        return it->second;
                     }
                  }
                  
                  int main() {
                     map <string,int> x;
                     ...
                     int i = GetWithDef( x, string("foo"), 42 );
                  }
                  

                  C++11 更新

                  用途:说明通用关联容器,以及可选的比较器和分配器参数。

                  template <template<class,class,class...> class C, typename K, typename V, typename... Args>
                  V GetWithDef(const C<K,V,Args...>& m, K const& key, const V & defval)
                  {
                      typename C<K,V,Args...>::const_iterator it = m.find( key );
                      if (it == m.end())
                          return defval;
                      return it->second;
                  }
                  

                  【讨论】:

                  • 不错的解决方案。您可能需要添加一些模板参数,以便函数模板与不使用比较器和分配器的默认模板参数的映射一起使用。
                  • +1,但要提供与 operator[] 完全相同的行为并使用默认值,应将默认值插入到 if ( it == m.end() ) 块内的地图中
                  • @David 我假设 OP 实际上并不想要这种行为。我使用类似的方案来读取配置,但如果缺少密钥,我不希望更新配置。
                  • @GMan bool 参数被某些人认为是不好的风格,因为您无法通过查看调用(而不是声明)来判断它们的作用 - 在这种情况下,“真”意味着“使用默认值”或“不使用默认值”(或完全其他)?枚举总是更清晰,但当然是更多的代码。我自己对这个问题有两种看法。
                  • 如果默认值为 nullptr,则此答案无效,但 stackoverflow.com/a/26958878/297451 有效。
                  【解决方案12】:
                  template<typename T, T X>
                  struct Default {
                      Default () : val(T(X)) {}
                      Default (T const & val) : val(val) {}
                      operator T & () { return val; }
                      operator T const & () const { return val; }
                      T val;
                  };
                  
                  <...>
                  
                  std::map<KeyType, Default<ValueType, DefaultValue> > mapping;
                  

                  【讨论】:

                  • 然后修改它,让它工作。我不会费心去解决这个代码不是为了完成而设计的案例。
                  【解决方案13】:

                  无法指定默认值 - 它始终是默认构造的值(零参数构造函数)。

                  事实上,operator[] 的作用可能超出了您的预期,就好像映射中给定键不存在值一样,它将插入一个具有默认构造函数值的新值。

                  【讨论】:

                  • 对,为避免添加新条目,您可以使用find,如果给定键不存在任何元素,它会返回结束迭代器。
                  • @ThomasSchaub 在这种情况下find 的时间复杂度是多少?
                  【解决方案14】:

                  C++ 标准 (23.3.1.2) 指定新插入的值是默认构造的,因此map 本身并没有提供这样做的方法。您的选择是:

                  • 为值类型提供一个默认构造函数,将其初始化为您想要的值,或者
                  • 将地图包装在您自己的类中,该类提供默认值并实现 operator[] 以插入该默认值。

                  【讨论】:

                  • 好吧,准确地说,新插入的值是初始化值(8.5.5),所以: - 如果 T 是具有用户声明的构造函数(12.1)的类类型,那么 T 的默认构造函数被调用(如果 T 没有可访问的默认构造函数,则初始化格式错误); — 如果 T 是没有用户声明的构造函数的非联合类类型,则 T 的每个非静态数据成员和基类组件都是值初始化的; — 如果 T 是一个数组类型,那么每个元素都是值初始化的; — 否则,对象为零初始化
                  【解决方案15】:

                  也许你可以给一个自定义分配器,用你想要的默认值进行分配。

                  template < class Key, class T, class Compare = less<Key>,
                         class Allocator = allocator<pair<const Key,T> > > class map;
                  

                  【讨论】:

                  • operator[] 返回一个通过调用T() 创建的对象,无论分配器做什么。
                  • @sbi: 地图不调用分配器construct 方法吗?我认为有可能改变这一点。不过,我怀疑construct 函数除了new(p) T(t); 之外的功能不是很好。编辑:事后看来这是愚蠢的,否则所有的价值观都是一样的:P 我的咖啡在哪里......
                  • @GMan:我的 C++03 副本说(在 23.3.1.2 中)operator[] 返回(*((insert(make_pair(x, T()))).first)).second。所以除非我遗漏了什么,否则这个答案是错误的。
                  • 你是对的。但这对我来说似乎是错误的。他们为什么不使用分配器功能进行插入?
                  • @sbi:不,我同意这个答案是错误的,但出于不同的原因。编译器确实使用T() 执行insert,但在插入时,它将使用分配器为新的T 获取内存,然后使用给定的参数在该内存上调用construct,即@987654333 @。所以确实有可能改变operator[] 的行为让它返回其他东西,但是分配器无法区分它被调用的原因。因此,即使我们让 construct 忽略它的参数并使用我们的特殊值,这也意味着 每个 构造的元素都有该值,这很糟糕。
                  猜你喜欢
                  • 2011-05-30
                  • 2019-04-01
                  • 2017-05-31
                  • 1970-01-01
                  • 2020-04-01
                  • 1970-01-01
                  • 1970-01-01
                  • 2016-02-22
                  • 2019-01-11
                  相关资源
                  最近更新 更多