3

I'm trying to figure out a smart-ass way to build a composite compile-time array for the AVR architecture. The array should be structured as follows:

  • it should reside entirely in program memory;
  • it is comprised of a continuous series of (unsigned) bytes, aka uint8_t;
  • it should be built using segments of bytes of arbitrary length;
  • a segment is comprised, in order, of a length byte and a series of data bytes, with the length byte being the number of data bytes.

Here's an example of such an array:

static const uint8_t data[] PROGMEM = {
    1, 0x01,
    3, 0xBE, 0x02, 0x00,
    3, 0x3D, 0x33, 0x33,
    15, 0xE1, 0xD0, 0x00, 0x05, 0x0D, 0x0C, 0x06, 0x2D, 0x44, 0x40, 0x0E, 0x1C, 0x18, 0x16, 0x19,
    0 /* end of the sequence */
};

I want to avoid the burden of adjusting the length byte everytime I'd add or remove a byte from the sequence, for instance, in some form of pseudo code:

BEGINNING_OF_THE_SEQUENCE(identifier)
    SEGMENT(0x01),
    SEGMENT(0xBE, 0x02, 0x00),
    ...
END_OF_THE_SEQUENCE()

In the above example I chose an explicit byte array declaration but it could be built in any way, for example using a structure, whatsoever. The only prerequisite is that the order of appearance must be guaranteed.

So in short I'd like to "concatenate" series of bytes, which length must be calculated at compile-time and put in front of each byte series as the length byte of the series itself.

I've considered using variadic macros but I'd also like to investigate other means, such as class and function templates, meta-programming, whatnot, with the smallest code in mind. I'd also like not to resort to C++11-specifics as the support with the current avr-gcc compiler I'm using is limited so far.

I've got a hunch it is possible using templates but I'm stuck. Any ideas?

3 Answers3

2

Here's a simple example for C++11 and beyond that might help:

template <typename ...Args>
constexpr std::size_t n_args(Args...) { return sizeof...(Args); }

#define ELEM(...) n_args(__VA_ARGS__), __VA_ARGS__

#include <iostream>

int main()
{
    unsigned int a[] = { ELEM(4, 9, 16),
                         ELEM(100),
                         ELEM(10, 20, 30, 40, 50),
    };

    for (auto n : a ) std::cout << n << " ";
    std::cout << '\n';
}

Alternatively, you could use sizeof a compound char-array literal in place of n_args, that's if you want a C99 solution rather than a C++11 one:

#define ELEM(...) sizeof((char[]){__VA_ARGS__}), __VA_ARGS__

I'm not aware of a similarly simple approach that would work in C++03.

Kerrek SB
  • 464,522
  • 92
  • 875
  • 1,084
  • It's ok, I checked your suggestion with `constexpr` and surprisingly enough avr-gcc 6.4 compiles this properly, putting everything in program memory. Thanks for pointing that out! –  Sep 30 '17 at 18:28
  • BTW I just wonder... is it possible to build this whole... thing (still at compile time) using templates instead of mixing templates and macros? –  Sep 30 '17 at 18:33
  • @Nasha Yes, it is possible, you should check my answer below. – user7860670 Sep 30 '17 at 18:34
  • @Nasha: Yes, but beware of your compile times. I don't know how big your application domain is, but unboundedly recursive variadic templates don't necessarily come cheap. I understand and have complete sympathy for your stance on macros, but I'd make a pragmatic trade-off here to find the appropriate solution. It also depends hugely on whether you need this just in one source file or as a reusable library, whether you can find a good, unique name for it, etc. Keep notes of the possible alternatives in the drawer. – Kerrek SB Sep 30 '17 at 21:37
  • Thanks for your insights @KerrekSB . I suspected compile times would raise — after all recursivity must take time somehow if not at runtime, then it'll be at compile time, that's logical. That's the reason I've flagged your answer as the solution. (And also because I'm still trying to understand how the templated solution works.) –  Sep 30 '17 at 21:41
  • @tambre: C++14, really, unless you spell out the return type of the function template. Sorry. I abbreviated too much, but the change for C++11 is trivial. – Kerrek SB Oct 01 '17 at 10:37
0

Here is one approach:

#include <stdint.h>
#include <stdio.h>
#define PROGMEM
#define _ARGSIZE(...) sizeof((uint8_t[]){__VA_ARGS__})/sizeof(uint8_t)
#define _SEGMENT(...)  _ARGSIZE( __VA_ARGS__ ), __VA_ARGS__
#define BEGINNING_OF_THE_SEQUENCE(__id)   uint8_t static const __id[] PROGMEM =  {
#define END_OF_THE_SEQUENCE() }

BEGINNING_OF_THE_SEQUENCE(data)
    _SEGMENT(0x01),
    _SEGMENT(0xBE, 0x02, 0x00),
    _SEGMENT(0xDE, 0xAD, 0xBE, 0xEF)
END_OF_THE_SEQUENCE();

int main() {
    int k, counter = data[0];
    for (k = 0; k < sizeof(data); k++) {
        fprintf(stderr, "%02x ", data[k]);
        if(counter-- == 0) {
            counter = data[1+k];
            fprintf(stderr, "\n");
        }
    }
}

This approach is C99 compatible.

The macros above can be modified with some care to take care of any type of data structure passed instead of uint8_t (including :P structures within structures)

Ahmed Masud
  • 21,655
  • 3
  • 33
  • 58
  • Thanks. I wrote something similar before I posted my message. It works perfectly both in C and C++. –  Sep 30 '17 at 18:38
0

Macro-free C++11 approach (v2):

#include <array>
#include <iostream>
#include <cstddef>
#include <cstdint>

//  This template will be instantiated repeatedly with VItems list
//  populated with new items.
template<typename TItem,  TItem... VItems> class
t_PackImpl
{
    //  This template will be selected for second and all other blocks.
    public: template<TItem... VInnerItems> using
    t_Pack = t_PackImpl
    <
        TItem
    //  add all previous items
    ,   VItems...
    //  add item holding amount of items in new block
    ,   TItem{static_cast<TItem>(sizeof...(VInnerItems))}
    //  add new block items
    ,   VInnerItems...
    >;

    //  This method will be called on the last instantiated
    //  template with VItems containing all the items.
    //  Returns array containing all the items with extra 0 item at the end.
    public: static constexpr auto
    to_array(void) -> ::std::array<TItem, sizeof...(VItems) + ::std::size_t{1}>
    {
        return {VItems..., TItem{}};
    }
};

//  This template will be instantiated just once.
//  Starts t_PackImpl instantiation chain.
template<typename TItem> class
t_BeginPack
{
    //  This template will be selected for first block.
    public: template<TItem... VInnerItems> using
    t_Pack = t_PackImpl
    <
        TItem
    //  add item holding amount of items in new block
    ,   TItem{static_cast<TItem>(sizeof...(VInnerItems))}
    //  add new block items
    ,   VInnerItems...
    >;
};

int main()
{
    {
        constexpr auto items
        {
            t_BeginPack<::std::uint8_t>::t_Pack<42>::to_array()
        };
        for(auto const & item: items)
        {
            ::std::cout << static_cast<::std::uint32_t>(item) << ::std::endl;
        }
    }
    ::std::cout << "----------------" << ::std::endl;
    {
        constexpr auto items
        {
            t_BeginPack<::std::uint8_t>::t_Pack<0, 1, 2>::to_array()
        };
        for(auto const & item: items)
        {
            ::std::cout << static_cast<::std::uint32_t>(item) << ::std::endl;
        }
    }
    ::std::cout << "----------------" << ::std::endl;
    {
        constexpr auto items
        {
            t_BeginPack<::std::uint8_t>::
                t_Pack<0, 1, 2>::
                t_Pack<0, 1>::
                t_Pack<0, 1, 2, 3, 4, 5>::to_array()
        };
        for(auto const & item: items)
        {
            ::std::cout << static_cast<::std::uint32_t>(item) << ::std::endl;
        }
    }
    return(0);
}

Run online

user7860670
  • 35,849
  • 4
  • 58
  • 84
  • I guess your answer involves meta programming, right? –  Sep 30 '17 at 18:42
  • @Nasha It does obviously since it involves templates. – user7860670 Sep 30 '17 at 18:44
  • I tend to avoid the STL in my mycrocontroller projects, that said. Also, I don't fully understand your answer, especially what the `using` keyword stands for. The construction details and that part of C++ programming are still obscure to me in fact. I naively believed I'd easily understand but I don't. Would you mind adding a detailed explanation on what it all does? Sorry, I feel like a noob there... –  Sep 30 '17 at 18:49
  • @Nasha Actually given approach does not depend on standard library that much, I've only used integer types (`uint8_t` and `size_t`) and `std::array` wrapper (which can be replaced with normal C-style array with few modifications). `using` keyword is used to declare [alias templates](http://en.cppreference.com/w/cpp/language/type_alias). Each in-class `t_Pack` template instantiates `t_PackImpl` template with new items added to `VItems`. – user7860670 Sep 30 '17 at 18:55
  • Ok. Now do I need the `to_array()` method? I still need to figure out how I can make the whole byte array `static const` to put it into the program memory section. –  Sep 30 '17 at 19:17
  • @Nasha `to_array()` method can be replaced with a regular static member variable (possibly of C array type). But it is good as it is. Notice that this method is `constexpr`, just like `items` arrays in `main`, that is it will be executed at compile time so it can be used to initialize `static const` without triggering any dynamic initialization calls at runtime. – user7860670 Sep 30 '17 at 19:22
  • would you mind showing how I could replace the `to_array()` with a static member variable? –  Sep 30 '17 at 19:29
  • @Nasha Well, that part is not fancy at all, just declare it and initialize like normal: `static constexpr ::std::array s_items{VItems..., TItem{}};` and then access as `::s_items` instead of `::to_array()`. – user7860670 Sep 30 '17 at 19:34
  • I'm asking for a few reasons, first being I'm unable to reproduce and explain your implementation on my own, second because with this... "construction type" I have no idea what to do, I really don't understand that part of C++ *at all*. Thirdly, I once tried to include STL templates and my code blew out of proportions, way beyond the limited size of the microcontroller program memory. As I mentioned, support for std-c++11 is only partial. –  Sep 30 '17 at 19:39
  • @Nasha Are you sure that executable growth is not caused by debug information? This code should yield exactly the same results as a hand-written array in your post. Additionally you can get rid of `` and `` headers completely and define resulting array as plain C array `static constexpr TItem s_items[sizeof...(VItems) + ::std::size_t{1}]{VItems..., TItem{}};` though that would be less flexible as C arrays are not assignable. – user7860670 Sep 30 '17 at 19:51
  • Yes, I'm sure it was not due to debug information. At the time I made the test, `avr-gcc` had very limited support for C++11. And there are still C++ features that lack implementation or have a rudimentary implementation as they don't make sense in a microcontroller environment. I don't know yet with `avr-gcc` version 6.4.0 because I haven't tried it in turn with that kind of exercise. –  Sep 30 '17 at 19:56
  • @Nasha avr-gcc 6.4 (as you've mentioned above) seems like a pretty fresh version that should have a decent level of C++11 support. I have nowhere to test it though. If it does not support C++ properly then I guess your only option would be to stick with macros. – user7860670 Sep 30 '17 at 20:14