【问题标题】:C++ Map Initialize object using non-default constructorC++ Map使用非默认构造函数初始化对象
【发布时间】:2018-09-12 07:29:56
【问题描述】:

在 C++ 中假设我有一个如下定义的无序映射:

unordered_map<int, MyClass> my_map;
auto my_class = my_map[1];

在上面的代码中,如果 my_map 中没有 1 作为键,它将使用默认构造函数初始化 MyClass 并返回。但是有没有办法使用 MyClass 的非默认构造函数进行初始化?

【问题讨论】:

  • 由于集合是key-only,您的my_set 声明无效。您的意思是改用std::unordered_map 吗?
  • my_set[1] = MyClass(...); 怎么样?这可能涉及副本。如果这不是故意的,那就有点复杂了。
  • 如果你的意思是std::unordered_map,那么除了拥有一个默认构造函数之外别无他法。
  • @Someprogrammerdude 是的,我的意思是 std::unordered_map。
  • 您应该创建一个 getMyMap(key, ...) 方法,该方法返回 my_map[key] 并在使用参数“...”进行构造之前创建它。否则我看不到其他办法。此外,我不建议养成使用 operator [] 映射的习惯,因为如果您在算法中出错(检索您认为存在的不存在的密钥),它将编译并运行,您将不知道为什么会得到结果错误。

标签: c++


【解决方案1】:

你说得对,operator[] 需要值类型是默认可构造的。

insert 没有:

std::unordered_map<int, MyClass> my_map;
// Populate the map here

// Get element with key "1", creating a new one
// from the given value if it doesn't already exist
auto result = my_map.insert({1, <your value here>});

这为您提供了一对包含元素的迭代器(无论是创建新的还是已经存在的)和一个布尔值(告诉您是哪种情况)。

所以:

auto& my_class = *result.first;
const bool was_inserted = result.second;

现在您可以使用这些信息做任何您想做的事情。通常你甚至不会关心result.second 并且可以忽略它。

对于更复杂的值类型,您可以使用emplace,它类似于insert,但是,嗯,更好。假设您真的不希望构造不使用的值,并且您拥有 C++17:

auto result = my_map.try_emplace(1, <your value's ctor args here here>);

如果你不关心(或没有 C++17):

auto result = my_map.emplace(1, <your value>);

这仍然比insert 好,因为它可以将值移动到地图中,而不是复制它。

最终,如果您甚至不想不必要地生成您的 ctor args,您总是可以先执行find,但最好尽量避免这种情况,因为插入操作本身会进行查找也是。

【讨论】:

  • "你总是可以先查找" - 或者使用 try_emplace 代替,如果 C++17 可用的话。这将使(至少在我看来)迄今为止的最佳答案完整......
  • @Aconcagua 您仍然必须准备好所有 ctor 参数,这可能是也可能不是问题。虽然可能很好
  • @Aconcagua 至少它是分段方法的替代品
  • 如果值类型的构造很昂贵,但参数很便宜,我们用 find 省去前面的查找。而且由于插入比分段的东西更简单(好吧,我现在可能会引用你自己的话从评论到 Scheff 的回答)......
  • 嗯,希望我没有学究气,但是“等效”在细节上是不正确的:虽然元素“即使已经存在[...]”也可以与前者一起创建,不会是后者……
【解决方案2】:

想象一下struct T

struct T {
  int i1, i2;
  // no default constructor
  explicit T(int i1, int i2): i1(i1), i2(i2) { }
};

使用默认构造函数非常简单:

aMap[123] = T(1, 23);

operator[] 允许按需创建一个不存在的条目(但为此它需要映射类型的默认构造函数)。


如果mapped_type 的类不提供默认构造函数,则可以通过std::unordered_map::find()std::unordered_map::insert() 的简单组合来匹配OP 的意图(或者仅通过后者检查成功)。

(这部分后来被插入,因为 A Lightness Races in Orbit 指出我跳过了这个简单的解决方案,直接转向更复杂的解决方案。)他为此写了alternative answer。由于它缺乏示范 MCVE,所以我拿走了我的并对其进行了改编:

#include <iostream>
#include <unordered_map>

struct T {
  int i1, i2;
  // no default constructor
  explicit T(int i1, int i2): i1(i1), i2(i2)
  {
    std::cout << "T::T(" << i1 << ", " << i2 << ")\n";
  }
};

int main()
{
  typedef std::unordered_map<int, T> Map;
  Map aMap;
  //aMap[123] = T(1, 23); doesn't work without default constructor.
  for (int i = 0; i < 2; ++i) {
    Map::key_type key = 123;
    Map::iterator iter = aMap.find(key);
    if (iter == aMap.end()) {
      std::pair<Map::iterator, bool> ret
        = aMap.insert(Map::value_type(key, T(1 + i, 23)));
      if (ret.second) std::cout << "Insertion done.\n";
      else std::cout << "Insertion failed! Key " << key << " already there.\n";
    } else {
      std::cout << "Key " << key << " found.\n";
    }
  }
  for (const auto &entry : aMap) {
    std::cout << entry.first << " -> (" << entry.second.i1 << ", " << entry.second.i2 << ")\n";
  }
  return 0;
}

输出:

T::T(1, 23)
Insertion done.
Key 123 found.
123 -> (1, 23)

Live Demo on coliru


如果映射类型也确实缺少复制构造函数,那么它仍然可以使用 std::unordered_map::emplace() 解决(再次使用或不使用 std::unordered_map::find() 进行预检查):

aMap.emplace(std::piecewise_construct,
  std::forward_as_tuple(123),
  std::forward_as_tuple(1, 23));

改编样本:

#include <iostream>
#include <unordered_map>

struct T {
  int i1, i2;
  // no default constructor
  explicit T(int i1, int i2): i1(i1), i2(i2)
  {
    std::cout << "T::T(" << i1 << ", " << i2 << ")\n";
  }
  // copy constructor and copy assignment disabled
  T(const T&) = delete;
  T& operator=(const T&);
};

int main()
{
  typedef std::unordered_map<int, T> Map;
  Map aMap;
  for (int i = 0; i < 2; ++i) {
    Map::key_type key = 123;
    Map::iterator iter = aMap.find(key);
    if (iter == aMap.end()) {
      std::pair<Map::iterator, bool> ret
        = aMap.emplace(std::piecewise_construct,
          std::forward_as_tuple(key),
          std::forward_as_tuple(1 + i, 23));
      if (ret.second) std::cout << "Insertion done.\n";
      else std::cout << "Insertion failed! Key " << key << " already there.\n";
    } else {
      std::cout << "Key " << key << " found.\n";
    }
  }
  for (const auto &entry : aMap) {
    std::cout << entry.first << " -> (" << entry.second.i1 << ", " << entry.second.i2 << ")\n";
  }
  return 0;
}

输出:

T::T(1, 23)
Insertion done.
Key 123 found.
123 -> (1, 23)

Live Demo on coliru

正如 Aconcagua 在评论中提到的,如果没有预先检查 find(),即使插入失败,emplace() 也可能会构造映射值。

文档。 cppreference 上的`std::unordered_map::emplace() 提到了这一点:

即使容器中已经有一个带有key的元素,也可以构造该元素,在这种情况下,新构造的元素将被立即销毁。


正如 Jarod42 提到的,std::unordered_map::try_emplace() 是 C++17 中值得一提的替代方案

与 insert 或 emplace 不同,如果插入未发生,这些函数不会从右值参数移动,这使得操作值是仅移动类型的映射变得容易,例如 std::unordered_map&lt;std::string, std::unique_ptr&lt;foo&gt;&gt;。另外try_emplacemapped_type的key和参数分开处理,不像emplace那样需要参数来构造一个value_type(也就是一个std::pair

【讨论】:

  • @Aconcagua 你是对的。 (我专注于“非默认构造”,因为我不同意 SomeProgrammerDude 的评论,但我想在意识到我的一些错误想象之前先检查一下。)但是,我编辑了答案。
  • (调整之前的,已删除的评论):emplacemight即使存在也构造MyClass元素;如果构建是一种廉价的操作,这可能很好,如果它很昂贵,我们可能更喜欢auto i = find; auto mc = i != end ? *i : *(emplace().first);...
  • 对于这些类型,这似乎是一个过于复杂的解决方案。为什么不只是insert
  • @LightnessRacesinOrbit 有很多方法可以将内容放入std::mapstd::unordered_map。其中一些非常容易,而另一些则更复杂,但在特殊情况下值得。示例struct T 用于演示非常简单(并保持代码尽可能小)。 emplace() 恕我直言,就地施工取得了巨大进步,因此值得一提。 (可能是,我应该明确提到这个事实。)您可以添加一个替代答案,显示使用insert() 会多么简单。 ;-)
  • @Scheff 我做到了。一般来说,您应该从简单的解决方案开始(尤其是在教学时),只有在需要时才变得复杂。这样你的代码会更清晰,更易于维护。
【解决方案3】:

[] 实现get_add_if_missing。从语义上讲,无开销的实现类似于:

value_type& get_add_if_missing(key_type const& k, auto&& factory) {
    auto b = bucket_for(k);
    auto pos = pos_for(k, b);
    if (pos == b.end()) {
        return b.append(k, factory());
    } else {
        return *pos;
    }
}

API 上还没有完整的等价物(截至C++17),所以现在,您需要根据创建临时value_type 的成本来决定要拥有哪些次优:

  • 做一个额外的查找(搜索然后插入,如果缺少)
  • 额外临时(insert/emplace 总是,在其他答案中很好地涵盖)

一个额外的查找版本是:

final itr = m.find(key);
if (itr == m.end()) {
    // insert or emplace a new element with the constructor of your choice
}

关于 cppreference 的 std::unordered_map 文章应该有足够的 insert / emplace 的使用示例。

对于基于树的实现 (std::map),get_add_if_missing 的零开销模拟是完全可能的

最后是好消息——如果您可以接受Boost.Intrusive(仅标头库)作为依赖项,您可以构建真正的零开销get_add_if_missing(无需临时或重复哈希计算)。那里的哈希映射 API 已经足够详细了。

【讨论】:

  • insert is already "add_if_missing";你不需要先找到
  • @LightnessRacesinOrbit - 是的,但您通常不希望不必要地构造 value_type,而仅使用 insert 是不可能的。
  • 好的 - 不清楚这是您对假设的 add_if_missing 的约束。无论如何,在这种情况下,从宏伟的计划来看,它似乎几乎没有成本。
  • 无论如何.. 那么,结合我的回答,这非常彻底。
  • 不,太复杂了,它们可以分开 :) 如果您只想快速记录一些关于不必要的构造的内容,您可以 +1 :P
猜你喜欢
  • 2023-03-14
  • 1970-01-01
  • 2012-01-02
  • 1970-01-01
  • 2017-05-31
  • 2011-08-09
  • 2018-12-08
  • 2011-05-15
  • 1970-01-01
相关资源
最近更新 更多