ESP32: The C++ map container

Introduction

In this post we will learn how to execute some basic operations on a std::map container. We will be using the ESP32 and the Arduino core.

A map is an associative container that stores elements formed by a combination of a key value and a mapped value, following a specific order [1]. Keys on a map should be unique [1].

In our introductory example we will be working with a map where the keys are going to be strings and the values are going to be integers. Naturally, you can use other types.

Please note that maps have a very rich API that offers a lot of flexibility when working with these containers. Additionally, there are operations that can be done in different but equivalent ways. In this tutorial we are just going to cover some basic operations (inserting, removing, searching and iterating through elements) and we are not going to list all the different possible ways to do all these operations. As such, the main objective is to create awareness for this data structure and give a simple introduction.

The tests shown below were performed on a ESP32-E FireBeetle board from DFRobot. The Arduino core version used was 2.0.0 and the Arduino IDE version was 1.8.15, working on Windows 8.1. The code was also tested on Platformio (the images below were taken from the tests on this platform).

A simple example on how to use a map

In this first example, we are going to learn how to create a map, insert some elements and iterate through all of them.

The first thing we will do is including the map header.

#include <map>

Then we will move on to the Arduino setup, where we start by creating our map. The default constructor will ensure that an empty map is created [1]. As type parameters, we need to specify the type of the keys and the type of the mapped values. For our simple use case, our keys will be of type std::string and our values of type int.

std::map<std::string, int> m;

Now we will insert some elements in our map. There are multiple ways of inserting these, but we will make use of the insert method. If you have a use case where the mapped values are objects, you may want to take a look at the emplace method for more efficiency. This StackOverflow thread also has a very interesting discussion about different insertion methods on the map.

It’s important to consider that the insert method checks whether each inserted element has a key equivalent to the one of an element already in the container and, if it is found, the element is not inserted, returning an iterator to this existing element [2]. If by some reason you need to support multiple entries with the same key, then you can explore the multimap.

To insert now our key and value, we are going to call the make_pair function, passing as first input our key and as second our value. This will create a pair object but deducing the template types from the arguments of the function [3]. We will then pass that pair as input of the insert method.

m.insert(std::make_pair("a", 1));

As an alternative, we could have created the pair object ourselves. In this case, the code will be a little bit longer since we need to indicate the template types.

m.insert(std::pair<std::string, int>("b", 2));

Now that we have our map with some elements, we will iterate through all of them and print both their keys and values. For that, we will use an iterator. The syntax to define an iterator is as follows:

<Container_Type> :: iterator;  

In our case, mapping to our map container, we have the following:

std::map<std::string, int>::iterator it;

To start the iteration, we can call the begin method on our map, which will return an iterator referring to the first element in the map container [4].

Similarly, to get a stopping condition, we can call the end method, which returns an iterator referring to the past-the-end element in the map container [5]. The past-the-end element is the theoretical element that would follow the last element in the map container [5], thus being usable as stop condition of a loop that iterates over all elements.

To make an iterator move to the next element of the container, we can use the ++ operator [6]. If we combine everything in a for loop, we can easily go over all the elements of the map.

for (it = m.begin(); it != m.end(); it++)
{
    // Print key and value
}

Since the elements of the map are exposed as pairs, the iterator will point to a pair. As such, we can access the key and the value using the first and second member variables, respectively.

Note that, since the key is a std::string and this type is not supported by the Serial print functions, we need to call the c_str method before passing the key to the print function.

Serial.println(it->first.c_str());
Serial.println(it->second);

The complete loop is shown below.

for (it = m.begin(); it != m.end(); it++)
{
  Serial.println(it->first.c_str());
  Serial.println(it->second);
}

Alternatively, we can also use a range-based for loop to iterate through all the elements of the map.

for (std::pair<std::string, int> element : m) {
  Serial.println(element.first.c_str());
  Serial.println(element.second);
}

The complete setup function is shown below and it has some additional prints for better readibility.

void setup() {
  
  Serial.begin(115200);

  std::map<std::string, int> m;

  m.insert({"a", 1});

  m.insert(std::make_pair("a", 1));
  m.insert(std::pair<std::string, int>("b", 2));

  std::map<std::string, int>::iterator it;

  Serial.println("Iterator");
  for (it = m.begin(); it != m.end(); it++)
  {
      Serial.print(it->first.c_str());
      Serial.print(": ");
      Serial.print(it->second);
      Serial.println();
  }

  Serial.println("Range based for loop");
  for (std::pair<std::string, int> element : m) {
      Serial.print(element.first.c_str());
      Serial.print(": ");
      Serial.print(element.second);
      Serial.println();
  }

}

The full code is available below.

#include <map>
  
void setup() {
  
  Serial.begin(115200);

  std::map<std::string, int> m;

  m.insert({"a", 1});

  m.insert(std::make_pair("a", 1));
  m.insert(std::pair<std::string, int>("b", 2));

  std::map<std::string, int>::iterator it;

  Serial.println("Iterator");
  for (it = m.begin(); it != m.end(); it++)
  {
      Serial.print(it->first.c_str());
      Serial.print(": ");
      Serial.print(it->second);
      Serial.println();
  }

  Serial.println("Range based for loop");
  for (std::pair<std::string, int> element : m) {
      Serial.print(element.first.c_str());
      Serial.print(": ");
      Serial.print(element.second);
      Serial.println();
  }

}
  
void loop() {}

As usual, to test the code, simply compile it and upload it using a tool of your choice (the Arduino IDE, platformio, etc..). Then, open a serial monitor tool.

You should see a result like the one in figure 1. As can be seen, both approaches allowed us to go through the map and obtain the previously inserted elements.

Inserting elements and iterating over a map container with the ESP32.
Figure 1 – Inserting elements and iterating over a map container with the ESP32.

Finding elements

In this section we will learn how to search for specific elements on our map. Like before, we start our code by the header file include.

#include <map>

Moving on to the Arduino setup, after opening the serial connection, we will take care of creating a new map and add three entries, for testing purposes. We will still use a string – integer map.

Serial.begin(115200);

std::map<std::string, int> m;

m.insert(std::make_pair("a", 1));
m.insert(std::make_pair("b", 2));
m.insert(std::make_pair("c", 3));

Now we will use the find method to search the container for an element with the provided key. This method returns an iterator to the element, in case it is found, or an iterator to map::end in case it is not found [7]. Consequently, we can use this information to confirm if the element was found or not, before trying to access it.

We will start by calling the find method and pass as input the “a” key, which we know exists in our map. Note that we will be using the C++ auto keyword to define the variable that will hold the result, so the compiler will infer its type by us. Naturally this is not mandatory, but it is a way of keeping the code shorter. If you prefer to declare the type, you can also do so.

auto a = m.find("a");

Then we will check if the element was found or not. We can obtain the end iterator with a call to the end method on our map. Then we will compare it against our result.

In case the element was found, we can get the key and the value like we did before: accessing the first and second member variables of the pair.

if(a!= m.end()){

  Serial.println("Element with key 'a' found:");
  Serial.print(a->first.c_str());
  Serial.print(":");
  Serial.print(a->second);
  Serial.println();
}else{
  Serial.println("Element with key 'a' not found.");
}

We will repeat the exercise, this time for a non-existing key. For this case, we will just print messages in the if and else blocks, because we know we won’t find the element.

auto z = m.find("z");
if(z!= m.end()){
    Serial.println("Element with key 'z' found:");
}else{
    Serial.println("Element with key 'z' not found.");
}

Other possibility to find the value of a given key is to use the at method, passing as input the key for which we want to obtain the mapped value. This method returns a reference to the mapped value of the element [8]. However, in this case, if the element is not found, an out_of_range exception is thrown [8].

As such, we will use this method to find an element we know exists.

int value = m.at("b");
Serial.println(value);

Finally, we will take a look at the [] operator. As input, we also need to pass the key for which we want to obtain the mapped value [9]. However, the behavior when the element is not found may be a bit unexpected. If the key provided doesn’t match the key of any element in the container, the method inserts a new element with that key and returns a reference to its mapped value. This always increases the container size by one, even if no mapped value is assigned to the element (the element is constructed using its default constructor) [9].

In the particular case of an int, if the element is not found, then 0 will be inserted / returned. We will test the operator usage for both an existing and a non-existing key.

int valueA = m["a"];
int valueZ = m["z"];

Serial.print("value of key a: ");
Serial.print(valueA);

Serial.print("\nvalue of key z: ");
Serial.print(valueZ);

To confirm this, we will finalize our Arduino setup by iterating through the map and confirm that a new key with the value “z” was added. Notice the usage of the auto keyword.

for (auto element : m) {
    Serial.print(element.first.c_str());
    Serial.print(": ");
    Serial.print(element.second);
    Serial.println();
}

The complete code is available below.

#include <map>
  
void setup() {
  
  Serial.begin(115200);

  std::map<std::string, int> m;

  m.insert(std::make_pair("a", 1));
  m.insert(std::make_pair("b", 2));
  m.insert(std::make_pair("c", 3));

  Serial.println("\nUsing find method");

  auto a = m.find("a");
  if(a!= m.end()){

    Serial.println("Element with key 'a' found:");
    Serial.print(a->first.c_str());
    Serial.print(":");
    Serial.print(a->second);
    Serial.println();
  }else{
    Serial.println("Element with key 'a' not found.");
  }

  auto z = m.find("z");
  if(z!= m.end()){
    Serial.println("Element with key 'z' found:");
  }else{
    Serial.println("Element with key 'z' not found.");
  }

  Serial.println("\nUsing at method");

  int value = m.at("b");
  Serial.println(value);

  Serial.println("\nUsing [] operator");
  int valueA = m["a"];
  int valueZ = m["z"];

  Serial.print("value of key a: ");
  Serial.print(valueA);

  Serial.print("\nvalue of key z: ");
  Serial.print(valueZ);

  Serial.println("\nIterating the map:");
  for (auto element : m) {
    Serial.print(element.first.c_str());
    Serial.print(": ");
    Serial.print(element.second);
    Serial.println();
  }

}
  
void loop() {}

The expected result is shown below in figure 2. As can be seen, using the find method, we were able to get the element with the “a” key and we were able to identify that “z” doesn’t exist, without getting an exception. With the at method we were also able to obtain the value of the “b” key.

Finally, using the [] operator, we were also able to retrieve the value for the “a” key, and we could confirm that, when the element with key “z” was not found, it was implicitly added to the map.

Finding elements in the map.
Figure 2 – Finding elements in the map.

Deleting elements

To finalize our tutorial, we will learn how to delete elements from the map.

Focusing our attention in the Arduino setup, where we will write all our code, we start by creating a map and add some elements to it, pretty much like we have been doing in the previous sections.

std::map<std::string, int> m;

m.insert(std::make_pair("a", 1));
m.insert(std::make_pair("b", 2));
m.insert(std::make_pair("c", 3));

Now, to delete an element from the map, we can use the erase method. The method is overloaded, meaning that it supports multiple signatures. We will use the simplest one, which receives as input the key of the element we want to delete.

m.erase("a");

Other option, still using the erase method, is to pass as input an iterator pointing to a single element to be removed from the map [10]. As such, for illustration purposes, we will use the find method first, to obtain an iterator to the element, and then pass this iterator as input of the erase method.

auto element = m.find("c");
m.erase(element);

To finalize the code, we will iterate through all the elements of the map, to confirm that the removals were successful.

for (auto element : m) {
    Serial.print(element.first.c_str());
    Serial.print(": ");
    Serial.print(element.second);
    Serial.println();
}

The full code can be obtained below.

#include <map>
  
void setup() {
  
  Serial.begin(115200);

  std::map<std::string, int> m;

  m.insert(std::make_pair("a", 1));
  m.insert(std::make_pair("b", 2));
  m.insert(std::make_pair("c", 3));

  m.erase("a");

  auto element = m.find("c");
  m.erase(element);

  Serial.println("\nIterating the map:");
  for (auto element : m) {
      Serial.print(element.first.c_str());
      Serial.print(": ");
      Serial.print(element.second);
      Serial.println();
  }

}
  
void loop() {}

Figure 3 shows the final result. As expected, we were able to remove the elements with keys “a” and “c”, and we got a map just with key “b”.

Printing the map after removing some of its elements.
Figure 3 – Printing the map after removing some of its elements.

Suggested ESP32 Readings

References

[1] https://www.cplusplus.com/reference/map/map/

[2] https://www.cplusplus.com/reference/map/map/insert/

[3] https://www.cplusplus.com/reference/utility/make_pair/

[4] https://www.cplusplus.com/reference/map/map/begin/

[5] https://www.cplusplus.com/reference/map/map/end/

[6] https://www.javatpoint.com/cpp-iterators

[7] https://www.cplusplus.com/reference/map/map/find/

[8] https://www.cplusplus.com/reference/map/map/at/

[9] https://www.cplusplus.com/reference/map/map/operator[]/

[10] https://www.cplusplus.com/reference/map/map/erase/

1 thought on “ESP32: The C++ map container”

Leave a Reply

Discover more from techtutorialsx

Subscribe now to keep reading and get access to the full archive.

Continue reading