tinkering with c++ — replacing boost array with standard c++ array

Boost does more than provide a collection of advanced libraries for the C++ language. Many of Boost’s authors are also part of the C++ standards committee. Thus, many new features being considered for the C++ standard are implemented here first. One of those features that has now migrated into the official standard is boost::array, which as of C++11 is now std::array. The page for boost::array has the following line on its documentation front page:

Update: std::array is (as of C++11) part of the C++ standard. The differences between boost::array and std::array are minimal. If you are using C++11, you should consider using std::array instead of boost::array.

The following code shows some of the ways this container is used. If you change the #include <array> to <boost/array>, as well as the corresponding using statement, then it will compile and execute the same.

#include <array>
#include <algorithm>
#include <iostream>

using std::array;
using std::cout;
using std::sort;
using std::string;

int main() {
    array<int, 10> iarray = 
        {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    cout << "Array of integers:" << "\n";
    for (const auto value : iarray) {
        cout << "Integer array value: " << value << "\n";
    }

    cout << "\n" << "Array of standard strings:" << "\n";
    array<string, 10> sarray =
        {"one","two","three","four","five","six","seven","eight","nine","ten"};

    for (const auto value : sarray) {
		    cout << "String array value: " << value << "\n";
    }

    cout << "\n" << "Array of sorted strings:" << "\n";
    sort(sarray.begin(), sarray.end());
    for (const auto value : sarray) {
		    cout << "String array value: " << value << "\n";
    }

    return 0;
}

Compiling It

Compilation is a simple one liner.

g++ -std=c++20 array-test.cpp -o array-test

Sample Run

Here’s what happens when we run it.

~ ./array-test 
Array of integers:
Integer array value: 1
Integer array value: 2
Integer array value: 3
Integer array value: 4
Integer array value: 5
Integer array value: 6
Integer array value: 7
Integer array value: 8
Integer array value: 9
Integer array value: 10

Array of standard strings:
String array value: one
String array value: two
String array value: three
String array value: four
String array value: five
String array value: six
String array value: seven
String array value: eight
String array value: nine
String array value: ten

Array of sorted strings:
String array value: eight
String array value: five
String array value: four
String array value: nine
String array value: one
String array value: seven
String array value: six
String array value: ten
String array value: three
String array value: two

Comments

What makes std::array better than std::vector is that there is no need for extra code to support a dynamic container like vector. The container array is like the simpler C fixed array, in that it’s fixed and static, but has enough container traits that it can be used in a C++ ranged for loop (lines 15,23, and 29), as show in the example above.

Using std::array in an Embedded Application

I have been writing ESP32-S3 and ESP32-C3 C++ code. I’ve published the following code more than once, but this time, I just want to highlight how I replaced the C++ standard library vector and tuple type with array. Here is where std::array helps in dropping the size of the binary. I checked the size of the generated binary (everything else the same) and discovered that I save a bit of 10K of generated binary, meaning that I saved 10K by switching to array. What’s more the ability to access the elements of the inner array in line 44 makes a lot more sense than the older way of declaring the inner colors a tuple and the convoluted way I accessed tuple members the way I did.

/**
  DualBlink

  This example code is in the Public Domain (or CC0 licensed, at your option.)

  Unless required by applicable law or agreed to in writing, this
  software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
  CONDITIONS OF ANY KIND, either express or implied.
**/
#include <stdio.h>
#include <array>

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "esp_log.h"
#include "led_strip.h"
#include "sdkconfig.h"

/* Create an array of color arrays to cycle through continuously.
 */
using std::array;
const array<array<int, 3>, 7> colors {{
    {32,0,0},  // red
    {0,32,0},  // green
    {0,0,32},  // blue
    {0,32,32}, // cyan
    {32,0,32}, // magenta
    {32,16,0}, // yellow
    {0,0,0}    // black
    }};

// Task 1.
//
static void task_blink_neo_pixel(void * pvParameters) {
    static led_strip_t *pStrip_a;
    pStrip_a = led_strip_init(CONFIG_BLINK_LED_RMT_CHANNEL, CONFIG_BLINK_GPIO, 1);
    pStrip_a->clear(pStrip_a, 50);

    // Stay in an endless loop. Don't return from this task.
    //
    while(true) {
        for(auto color : colors) {
            pStrip_a->set_pixel(pStrip_a, 0, color[0], color[1], color[2]);
            pStrip_a->refresh(pStrip_a, 100);
            vTaskDelay(500 / portTICK_PERIOD_MS);
        
            // Set NeoPixel LED dark by clearing all its individual LEDs.
            pStrip_a->clear(pStrip_a, 50);
            vTaskDelay(500 / portTICK_PERIOD_MS);
        }
    }
}

// Task 2.
//
static void task_blink_led(void * pvParameters) {
    gpio_reset_pin(GPIO_NUM_46);

    // Set the GPIO as a push/pull output
    //
    gpio_set_direction(GPIO_NUM_46, GPIO_MODE_OUTPUT);

    // Stay in an endless loop. Don't return from this task.
    //
    while (true) {
        gpio_set_level(GPIO_NUM_46, true);   // LED on
        vTaskDelay(100 / portTICK_PERIOD_MS);
        gpio_set_level(GPIO_NUM_46, false);  // LED off
        vTaskDelay(100 / portTICK_PERIOD_MS);
        gpio_set_level(GPIO_NUM_46, true);   // LED on
        vTaskDelay(100 / portTICK_PERIOD_MS);
        gpio_set_level(GPIO_NUM_46, false);  // LED off
        vTaskDelay(2700 / portTICK_PERIOD_MS);
    }
}

extern "C" void app_main(void) {
    static const char *TAG = "DUAL_BLINK";
    int core = xPortGetCoreID();
    ESP_LOGI(TAG, "app_main running on core %i", core);
    ESP_LOGI(TAG, "CONFIG_BLINK_GPIO %i", CONFIG_BLINK_GPIO);

    // Create task 1.
    //
    TaskHandle_t xHandle1 = NULL;
    static uint8_t ucParameterToPass1 = 1;

    xTaskCreate(
        task_blink_neo_pixel,
        "BlinkNeoPixel",        // human readable task name.
        2048,                   // stack size in bytes.
        &ucParameterToPass1,
        tskIDLE_PRIORITY,
        &xHandle1
    );
    configASSERT(xHandle1);

    // Create task 2.
    //
    TaskHandle_t xHandle2 = NULL;
    static uint8_t ucParameterToPass2 = 1;

    xTaskCreate(
        task_blink_led,
        "BlinkLED",             // human-readable task name.
        2048,                   // stack size in bytes.
        &ucParameterToPass2,
        tskIDLE_PRIORITY,
        &xHandle2
    );
    configASSERT(xHandle2);

    // Stay in an endless loop. Don't return from this task.
    //
    while (true) {
        vTaskDelay(1000 / portTICK_PERIOD_MS);
    }
}

tinkering with c++ — boost header file only programming

I’m presenting a C++ application that uses the header-file-only Boost libraries, in this case Multiprecision. The link to the Boost documentation for this library is at the bottom of this post.

#include <algorithm>
#include <iostream>
#include <iomanip>
#include <stdexcept>
#include <filesystem>
#include <string>
#include <boost/multiprecision/cpp_int.hpp>

using std::cout;
using std::cerr;
using std::left;
using std::right;
using std::setw;
using std::fixed;
using std::stoi;
using std::string;
using std::filesystem::path;
using std::invalid_argument;

using boost::multiprecision::cpp_int;

void factorial(cpp_int n) {
    cpp_int a = 1;

    cout << right << setw(4) << fixed << n;

    while ( n > 0 ) {
        a = a * n;
        n--;
    }

    cout << left << "! = " << a << "\n";
}

int main(int argc, char *argv[]) {
    string appname = path(argv[0]).stem();
    if (argc == 1) {
        cout << appname << " can be invoked with one or more integer arguments.\n";
        cout << "Example: " << appname << " 10 20 30 40 50 60 70 80 90 100\n";
    }

    int index{1};
    try {
        for (; index < argc; factorial(stoi(argv[index++])));
    }
    catch(invalid_argument ia) {
        index--;
        cerr << apname << " invalid argument: " << argv[index] << "\n";
        return -1;
    }

    return 0;
}

General Description

You’ve seen this code before, but in a far simpler code format than this. This version of the application has a try/catch block at lines 43 and 46 to catch an exception that stoi will throw if it’s given a non-numeric argument to parse. The catch statement explicitly catches an invalid argument and will print out what caused the application to stop. This is a lot better than the core dump it performed before this. Furthermore, if you call this application without any arguments it will give you several lines of help (at least, I hope they’re helpful). I’ve pulled a little bit of C++ slight-of-hand here by using std::filesystem::path to pull out the name of the application, or at least the name it was invoked by. This is an old trick in bash scripts and Python, but I’m using functionality from the C++ 17 standard.

How to Build

Building this is a one-liner:

g++ -std=c++20 SimpleFactorial.cpp -o SimpleFactorial

You’ll note that I didn’t use the -static switch. Since there were no specific Boost libraries to link, I decided to just build a regular Linux application that will dynamically link against standard Linux C and C++ libraries. Those are common to every modern Linux distribution.

Sample Runs

The first example shows what happens when you invoke SimpleFactorial without any arguments.

~ ./SimpleFactorial 
SimpleFactorial can be invoked with one or more integer arguments.
Example: SimpleFactorial 10 20 30 40 50 60 70 80 90 100

So what happens if we invoke that example?

~ ./SimpleFactorial 10 20 30 40 50 60 70 80 90 100
  10! = 3628800
  20! = 2432902008176640000
  30! = 265252859812191058636308480000000
  40! = 815915283247897734345611269596115894272000000000
  50! = 30414093201713378043612608166064768844377641568960512000000000000
  60! = 8320987112741390144276341183223364380754172606361245952449277696409600000000000000
  70! = 11978571669969891796072783721689098736458938142546425857555362864628009582789845319680000000000000000
  80! = 71569457046263802294811533723186532165584657342365752577109445058227039255480148842668944867280814080000000000000000000
  90! = 1485715964481761497309522733620825737885569961284688766942216863704985393094065876545992131370884059645617234469978112000000000000000000000
 100! = 93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000

What happens if we invoke SimpleFactorial with bad input data?

~ ./SimpleFactorial 10 20 foo
  10! = 3628800
  20! = 2432902008176640000
SimpleFactorial invalid argument: foo

There’s another “hidden” feature of the application. What happens if you rename SimpleFactorial to something else, invoking it differently than it’s original name? What if you rename your source and build the application as something else? That’s where the path function comes into play. Let’s see what happens if we change the name of the application by way of a soft link.

~ ln -s SimpleFactorial foo
~ ./foo
foo can be invoked with one or more integer arguments.
Example: foo 10 20 30 40 50 60 70 80 90 100

What happens when we have an error with this new name?

~ ./foo bar
foo invalid argument: bar

This capability doesn’t seem all that important, but for more complex applications it comes in quite handy when you’re chasing down errors from within a long log file.

If you’re wondering why I didn’t use Boost’s Program Options, it’s because I didn’t understand the documentation well enough to learn how to pass an arbitrary number of arguments to the application. Maybe later…

Link: https://www.boost.org/doc/libs/1_80_0/libs/multiprecision/doc/html/index.html