std::map 中缓存的 C++ 11 最佳实践?

C++ 11 Best practices for caching in a std::map?

(在 C++ 11 中)

我想在地图中存储计算成本较高的对象 (Product)。这些对象 space 并不便宜,所以我不想创建不必要的副本。该地图属于 Container class,可以访问对象。我做了一个代表我所拥有的例子(代码也可以在这里找到:run online):

#include <iostream>
#include <map>
#include <string>
#include <utility>

class Product
{
private:
  std::string m_id, m_name;

public:
  Product () : m_id (), m_name () { std::cout << "\tProduct default constructor.\n"; }

  Product (const std::string & id, const std::string & name) : m_id (id), m_name (name)
  {
    std::cout << "\tProduct parameters constructor <" << m_id << ", " << m_name << ">.\n";
  }

  Product (const Product & copy) : m_id (copy.m_id), m_name (copy.m_name)
  {
    std::cout << "\tProduct copy constructor.\n";
  }
};

class Container
{
private:
  std::map<std::string, Product> m_products_cache;

public:
  Container () : m_products_cache () { }
  Container (const Container & copy) : m_products_cache (copy.m_products_cache) { }

  Container (const std::string & filename) : Container ()
  {
    // Simulate reading file and storing its contents in map with this:
    m_products_cache.insert (std::pair<const std::string, Product> ("A-001", Product ("A-001", "Product 1")));
    m_products_cache.insert (std::pair<const std::string, Product> ("A-002", Product ("A-002", "Product 2")));
  }

  const Product &
  CreateNewProduct (const std::string & id, const std::string & name)
  {
    std::map<std::string, Product>::iterator product_it = m_products_cache.find (id);

    if (product_it != m_products_cache.end ())
      return product_it->second; // Returns a const-reference to the Product

    std::pair<std::map<std::string, Product>::iterator, bool> inserted_it;
    inserted_it = m_products_cache.insert (std::pair<const std::string, Product> (id, Product (id, name)));

    return inserted_it.first->second; // Returns a const-reference to the Product
  }

  // Case (i)  :  Returns a const-reference.
  const Product &
  GetProductById (const std::string & id) const
  {
    std::map<std::string, Product>::const_iterator product_it = m_products_cache.find (id);

    if (product_it == m_products_cache.end ())
      throw std::out_of_range ("Error: Product ID'" + id + "' NOT found.\n");

    return product_it->second;
  }

  // Case (ii)  :  Uses an non-const reference parameter to return the Product, if found.
  bool
  GetProductById (const std::string & id, Product & product) const
  {
    std::map<std::string, Product>::const_iterator product_it = m_products_cache.find (id);

    if (product_it == m_products_cache.end ()) return false;

    product = product_it->second;
    return true;
  }

  // Case (iii)  :  Uses a const Product non-const pointer reference to expose the Product, if found.
  bool
  GetProductById (const std::string & id, const Product *& product) const
  {
    std::map<std::string, Product>::const_iterator product_it = m_products_cache.find (id);

    if (product_it == m_products_cache.end ())
      {
        product = nullptr;
        return false;
      }

    product = &product_it->second;
    return true;
  }
};

int
main (int argc, char **argv)
{
  Container container ("ignored_filename");

  const Product & created_product = container.CreateNewProduct ("B-003", "Product 3");
  std::cout << "\n - Created Product located at " << &created_product << "\n\n";

  std::cout << " - Obtain reference to Product in container:\n\n";

  const Product & product_reference = container.GetProductById ("B-003"); // Gets product at expected address.
  std::cout << "\tCase i  : product located at " << &product_reference
            << (&product_reference == &created_product ? " (Same object)" : " (Different object)") << "\n";

  Product product_param;
  container.GetProductById ("B-003", product_param); // Question 1: Gets a copy, but doesn't call copy constructor, WHY?
  std::cout << "\tCase ii : product located at " << &product_param
            << (&product_param == &created_product ? " (Same object)" : " (Different object)") << "\n";

  const Product *product_pointer_param;
  container.GetProductById ("B-003", product_pointer_param); // Gets product at expected address.
  std::cout << "\tCase iii: product located at " << product_pointer_param
            << (product_pointer_param == &created_product ? " (Same object)" : " (Different object)") << "\n";
}

产生以下输出:

 - Created Product located at 0x6000727b8

 - Obtain reference to Product in container:

    Case i  : product located at 0x6000727b8 (Same object)
    Product default constructor.
    Case ii : product located at 0xffffcb40 (Different object)
    Case iii: product located at 0x6000727b8 (Same object)

我有以下问题:

  1. 问题 1:此代码没有 return 对 0x6000727b8 的引用,而是创建了一个副本,该副本存储在0xffffcb40,但是Product的拷贝构造函数没有被调用,为什么?

    // Case (ii): Output reference parameter
    Product product_param;
    container.GetProductById ("B-003", product_param);
    
  2. 问题 2:我想提供对地图中对象的访问,以避免创建对象的副本。如果找到 Product,我还必须用布尔值或异常来指出。我更喜欢 case (ii): bool GetProductById (const std::string & id, Product & product) 之类的东西,但这并不强制执行常量。

    我在代码中提供的三种情况,或者您的另一个建议,哪种是实现它的最佳实践?

  3. 问题3:这种情况下,map中存储的Product存储后不会发生变化。但是,如果 Product 在存储在地图中后要更改,那么问题 2 中的方法是否仍然适用?直接更改地图值对象是否是一种不好的做法? (我知道键必须是常量)。

  1. 您有 2 个对象:product_param 和存储在地图中的对象。当你通过引用 product 赋值给 product_param 时,它将使用赋值运算符 Product::operator=(const Product&) (编译器为你提供,因为你没有告诉它不要这样做)来赋值内容。您仍然有 2 个对象,它们每个都有自己的 ID 和名称。

  2. case (i) 适用于抛出异常的情况。 case (iii) 有点笨拙但有效。另一个选项是 const Product* GetProductById(const std::string& id) const。这可以 return 一个指向对象的指针(如果它存在)或者 return nullptr 如果它不存在。不幸的是,除了 return 值之外,它与 case (i) 具有相同的原型,因此您需要使用不同的函数名称或以某种方式想出其他方法区分如何称呼它们。

    标准通常这样做的方式是对不抛出的版本使用 operator[](const Key& key),对抛出的版本使用 at(const Key& key)。一个指向 return 指针而另一个指向 return 引用会有点尴尬。

  3. 这完全取决于您。没有什么可以阻止您更改地图的值,但如果您不希望用户能够这样做,那么您始终可以 return const 引用或指向 const 的指针返回给用户。