首页 > 解决方案 > Range based for loops without using std libraries

问题描述

I am working on an arduino project where I need to do the same task for a number of digital outputs. Since the arduino compiler supports C++11, I would like to use range-based for loops to have more readable code. However, as far as I know, on arduino there is no access to the std libraries that one would usually use in this case (correct me, if I'm wrong here). So my question is, whether there are good approaches to use range-based for loops without the std libraries. My (simplified) working code looks as follows:

static const byte OUTPUT_GROUP1[] = {4};
static const byte OUTPUT_GROUP2[] = {5, 6, 7};
static const byte *OUTPUTS[] = {OUTPUT_GROUP1, OUTPUT_GROUP2};

static const uint32_t MILLIS_GROUP1[] = {1000};
static const uint32_t MILLIS_GROUP2[] = {5000, 2000, 3000};
static const uint32_t *MILLIS[] = {MILLIS_GROUP1, MILLIS_GROUP2};

static const size_t GROUP_SIZES[] = {1, 3};

void loop() {
    for (size_t group = 0; group < 2; ++group) {
        for (size_t i = 0; i < GROUP_SIZES[group]; ++i) {
            digitalWrite(OUTPUTS[group][i], HIGH);
            delay(MILLIS[group][i]);
            digitalWrite(OUTPUTS[group][i], LOW);
        }
    }
}

So there are two groups of outputs that I would like to keep apart. My main goal is to get rid of the variable GROUP_SIZES, since the number of outputs per group may change and forgetting about updating GROUP_SIZES is a source of error. For that I need to achieve two things. Firstly, I need to loop over nested lists, which usually I might solve using a std::vector. Secondly, I need to loop over two ranges simultaneously, to also access the millis to delay. This could be done using std or boost. However, on arduino it seems that there is no easy approach without coding a lot of magic myself. Is there an easy way to get this working?

标签: for-loopc++11arduino

解决方案


The code example in this answer only works if you compile in C++17 mode - see how-to at the end of the answer


First, I recommend restructuring the data slightly. You have an output pin and an accociated millis time that you now store in separate arrays. I'd put them in a struct:

struct Output {
    byte pin;
    uint32_t ms;
};

You also need a container that can store objects and that has the begin() and end() member functions. I've put together an embryo, staticvector, which is sort of the worst of std::vector and std::array combined - but it's simple. The reason I didn't make it more like a std::array is that it should be able to store arrays of staticvectors of different sizes. The reason for not making it more like std::vector is because it requires a lot more code.

To make it easier to follow I've copied a few of the possible implementations of some standard templates from https://cppreference.com/ and put them in a namespace of their own. You can read about the templates on https://cppreference.com/ if they are unfamiliar.

namespace xyz {
template<class T> struct remove_reference      {using type = T;};
template<class T> struct remove_reference<T&>  {using type = T;};
template<class T> struct remove_reference<T&&> {using type = T;};
template<class T>
using remove_reference_t = typename remove_reference<T>::type;

template<class T>
constexpr T&&  forward(remove_reference_t<T>& t) noexcept { return static_cast<T&&>(t); }
template<class T>
constexpr T&&  forward(remove_reference_t<T>&& t) noexcept { return static_cast<T&&>(t); }

template<class T>
constexpr remove_reference_t<T>&&  move(T&& t) noexcept {
    return static_cast<remove_reference_t<T>&&>(t);
}

template<class T, class U = T>
constexpr T exchange(T& obj, U&& new_value) {
    T old_value = move(obj);
    obj = forward<U>(new_value);
    return old_value;
}
} // namespace xyz

With those helper templates, the actual container could look like this:

template<class T>
class staticvector {
private:
    size_t len = 0;
    T* data = nullptr;

public:
    // a constructor taking one or more T's and storing them in "data"
    template<class U, class... V>
    constexpr staticvector(U&& u, V&&... vs) :
        len(sizeof...(V) + 1),
        data{new T[sizeof...(V) + 1]{xyz::forward<U>(u), xyz::forward<V>(vs)...}}
    {}

    // copy constructor
    staticvector(const staticvector& rhs) :
        len(rhs.len),
        data(new T[len])
    {
        auto rhs_it = rhs.data;
        for(auto it = data, end = data + len; it != end; ++it, ++rhs_it) {
            *it = *rhs_it;
        }
    }

    // move constructor
    staticvector(staticvector&& rhs) noexcept :
        len(rhs.len), data(xyz::exchange(rhs.data, nullptr))
    {}

    // implement this if you wish
    staticvector& operator=(const staticvector&) = delete; // copy assignment
    
    // move assignment
    staticvector& operator=(staticvector&& rhs) noexcept {
        len = rhs.len;
        // this could use `std::swap(data, rhs.data)`
        auto *tmp = data;        
        data = rhs.data;
        rhs.data = tmp;
    }

    ~staticvector() { delete[] data; } // destructor

    size_t size() const { return len; }

    // subscript operator
    const T& operator[](size_t idx) const { return data[idx]; }
    T& operator[](size_t idx) { return data[idx]; }
    
    // iterator support
    using const_iterator = const T*;
    using iterator = T*;

    const_iterator cbegin() const { return data; }
    const_iterator cend() const { return data + len; }
    const_iterator begin() const { return cbegin(); }
    const_iterator end() const { return cend(); }
    iterator begin() { return data; }
    iterator end() { return data + len; } 
};

Now you can create a staticvector containing Output objects and also a container containing such containers.

using OutputGroup = staticvector<Output>; // a convenience alias

// All your arrays put into a container of containers
static const staticvector<OutputGroup> OUTPUTS{
    OutputGroup{Output{4, 1000}},
    OutputGroup{Output{5, 5000}, Output{6, 2000}, Output{7, 3000}}
};

void loop() {

    // Two range-based for loops:
    for (const OutputGroup& group : OUTPUTS) {

        // structured bindings from each Output object in `group`
        for (auto[pin, ms] : group) {
            digitalWrite(pin, HIGH);
            delay(ms);
            digitalWrite(pin, LOW);
        }
    }
}

Demo


Using C++17 in Arduino projects

Current Arduino IDE uses g++ 7.3.0 and supports C++17 - but it's a bit messy to set it up.

  • Find and edit platform.txt - I found it in <installation_directory>/hardware/avr/1.8.3 but it may vary.
  • Find the line that starts with compiler.cpp.flags=
  • Change -std=gnu++11 to -std=gnu++17 on that line.

Important note: Programs using delete[] (like the above) will fail to link in C++17 mode because the arduino library doesn't include operator delete[](void* ptr, size_t), even in the Arduino IDE 2.0-beta.

The current workaround seems to be to add your own to the project:

void operator delete[](void* ptr, size_t) noexcept { free(ptr); }

With the above fixes, the above program compiled for Uno with these stats:

Sketch uses 2052 bytes (6%) of program storage space. Maximum is 32256 bytes.
Global variables use 43 bytes (2%) of dynamic memory, leaving 2005 bytes for local variables. Maximum is 2048 bytes.

推荐阅读