C++20 how to deal with multi-leveled unordered_map

111 Views Asked by At

I have a complex multi-layered dictionary in Python, and I want to know how to "translate" such thing to C++. I am just a beginner in C++ so I use C++20 standard, my IDE is Visual Studio 2022 and I want to know if in C++20 there is a simpler way to do it.

In this example, the first level keys are strings, and the first level values are all dictionaries of similar structures. The second level keys are all strings, but the values can be integers, lists and dictionaries. If the third level is a list, it is a list of integers, else if the last structure is a dictionary, it is a dictionary mapping integers to integers.

Example in Python:

{
    "         ": {
        "over": False,
        "wins": 131184,
        "ties": 46080,
        "legal_moves": [
            0,
            1,
            2,
            3,
            4,
            5,
            6,
            7,
            8
        ],
        "win_moves": {
            0: 14652,
            1: 14232,
            2: 14652,
            3: 14232,
            4: 15648,
            5: 14232,
            6: 14652,
            7: 14232,
            8: 14652
        },
        "tie_moves": {
            0: 5184,
            1: 5184,
            2: 5184,
            3: 5184,
            4: 4608,
            5: 5184,
            6: 5184,
            7: 5184,
            8: 5184
        },
        "high_stakes": {
            0: 9468,
            1: 9048,
            2: 9468,
            3: 9048,
            4: 11040,
            5: 9048,
            6: 9468,
            7: 9048,
            8: 9468
        }
    }
}

The example shows all ways the first player can win from the starting state in Tic-Tac-Toe, I have computed this for all 5478 states reachable from the starting start if player O moves first, I have also computed the same if the other moves first, for a total of 8533 states.

The data shows whether the game is over, all ways player O can win from the state, and all ways the state can lead to a tie state, all legal moves, and for each move all possible ways the move can lead to a win, or a tie. "high_stake" means all possible ways the player can win minus all possible ways the game ends in a tie, negative values are set to 0.

I computed them in Python several days ago, I didn't use C++ because I don't know how to declare this structure in C++.

In C++ syntax, the data would be:

{
    {"         ", {
        {"over", 0},
        {"wins", 131184},
        {"ties", 46080},
        {"legal_moves", {
            0,
            1,
            2,
            3,
            4,
            5,
            6,
            7,
            8
        }},
        {"win_moves", {
            {0, 14652},
            {1, 14232},
            {2, 14652},
            {3, 14232},
            {4, 15648},
            {5, 14232},
            {6, 14652},
            {7, 14232},
            {8, 14652}
        }},
        {"tie_moves", {
            {0, 5184},
            {1, 5184},
            {2, 5184},
            {3, 5184},
            {4, 4608},
            {5, 5184},
            {6, 5184},
            {7, 5184},
            {8, 5184}
        }},
        {"high_stakes", {
            {0, 9468},
            {1, 9048},
            {2, 9468},
            {3, 9048},
            {4, 11040},
            {5, 9048},
            {6, 9468},
            {7, 9048},
            {8, 9468}
        }}
    }
    }
}

What would be the type identifier of this monstrosity? Know I have 5477 other such structures in the same dictionary.

I know how to declare the individual objects:

#include <unordered_map>
#include <string>


using std::unordered_map
using std::string

unordered_map<int, int> high_stakes = {
            {0, 9468},
            {1, 9048},
            {2, 9468},
            {3, 9048},
            {4, 11040},
            {5, 9048},
            {6, 9468},
            {7, 9048},
            {8, 9468}
        };


unordered_map<string, unordered_map<int, int>> d1 = {{"high_stakes", {
            {0, 9468},
            {1, 9048},
            {2, 9468},
            {3, 9048},
            {4, 11040},
            {5, 9048},
            {6, 9468},
            {7, 9048},
            {8, 9468}
        }}};

unordered_map<string, int[9]> d2 = {{"legal_moves", {
            0,
            1,
            2,
            3,
            4,
            5,
            6,
            7,
            8
        }}};

How can I declare these objects? I am going to compute the results in C++, and serialize the objects to a file. The statistics will be computed once, and other C++ programs will deserialize the results.

How can I guarantee the type won't be changed? I know I can't use JSON or any human-readable text based serialization format. I actually used pickle to store the data and that is what my Python program actually loads. What binary serialization format provided by the C++ standard library should I use for this purpose?


I know JSON cannot have integers as keys, I need the integer keys stay integers. Plus, I have benchmarked it, JSON serialization is slow.

1

There are 1 best solutions below

0
wu1meng2 On BEST ANSWER

Consider using Boost PropertyTree library. It has a JSON parser to convert an input JSON file to a boost::property_tree::ptree object. You can add and access nodes easily.

If you don't want to start from a JSON, you can manually add nodes to a boost::property_tree::ptree. Since Python 3.7, dict preserves the insertion order, which makes Boost Property an ideal counterpart in C++. I think the keys and values are defaulted to strings upon retrieval, but you can easily cast type with the utility function get_value<T>().

Anyway, here is an example Live on Coliru based on your JSON-like input with a few modifications,

#include <iostream>
#include <sstream>

#include <boost/property_tree/json_parser.hpp>
#include <boost/property_tree/ptree.hpp>

namespace pt = boost::property_tree;

int main() {
  std::string json_str = R"(
  {
      "info": {
          "over": false,
          "wins": 131184,
          "ties": 46080.5,
          "legal_moves": [
              0,
              1
          ],
          "win_moves": {
              "0": 14652,
              "1": 14232
          }
      }
  }
  )";

  // Parse the JSON string
  std::stringstream json_stream(json_str);
  pt::ptree root;
  pt::read_json({json_stream}, root);

  // Access value of a leave node
  {
    auto over = root.get_child("info").get_child("over").get_value<bool>();
    std::cout << "over = " << over << std::endl;
  }

  // boolean
  auto over = root.get_child("info.over").get_value<bool>();
  std::cout << "over = " << over << std::endl;

  // integer number
  auto wins = root.get_child("info.wins").get_value<int>();
  std::cout << "wins = " << wins << std::endl;

  // floating-point number
  auto ties = root.get_child("info.ties").get_value<double>();
  std::cout << "ties = " << ties << std::endl;

  // array of numbers
  std::cout << "legal_moves:\n";
  for (const auto &it : root.get_child("info.legal_moves")) {
    std::cout << it.second.get_value<int>() << std::endl;
  }

  // nested object
  std::cout << "win_moves:\n";
  for (const auto &it : root.get_child("info.win_moves")) {
    auto key = it.first;
    auto val = it.second.get_value<int>();
    std::cout << key << " : " << val << std::endl;
  }
}

Note that boost::property_tree::read_json() preserves the original order of values, while another popular package JsonCpp will sort the values due to the underlying std::map<> container.

I modified your original JSON-like string a bit:

  1. boost::property_tree::read_json() requires that JSON keys be strings.
  2. If you insist on using " " as a key, call get_child(" ").get_child("...") to get child nodes.
  3. False => false to allow get_value<bool>(); otherwise, you have to parse it as a string and then convert to bool manually.