0

Consider the following class:

template <size_t nb_rows, size_t nb_cols>
class ButtonMatrix
{
  public:
    ButtonMatrix(const pin_t (&rowPins)[nb_rows], const pin_t (&colPins)[nb_cols], const uint8_t (&addresses)[nb_rows][nb_cols])
      : rowPins(rowPins), colPins(colPins), addresses(addresses) { }
  private:
    const pin_t (&rowPins)[nb_rows], (&colPins)[nb_cols];
    const uint8_t (&addresses)[nb_rows][nb_cols];
};

I can initialize it using arrays like this:

const pin_t rows[2] = {0, 1};
const pin_t cols[2] = {2, 3};

const uint8_t addresses[2][2] = {
  {0x01, 0x02},
  {0x03, 0x04}
};

ButtonMatrix<2, 2> bm(rows, cols, addresses);

And it works just fine. However, I'd like to be able to initialize it using brace-enclosed initializer lists as well:

ButtonMatrix<2, 2> bm({0, 1}, {2, 3}, addresses);

It compiles without problems, but it obviously doesn't work, because the rowPins and colPins only live for the duration of the constructor, and cannot be used in the other methods of the class. To get around this, I could copy the contents of rowPins and colPins:

template <size_t nb_rows, size_t nb_cols>
class ButtonMatrix
{
  public:
    ButtonMatrix(const pin_t (&rowPins)[nb_rows], const pin_t (&colPins)[nb_cols], const uint8_t (&addresses)[nb_rows][nb_cols])
      : addresses(addresses) {
      memcpy(this->rowPins, rowPins, sizeof(rowPins[0]) * nb_rows);
      memcpy(this->colPins, colPins, sizeof(colPins[0]) * nb_cols);
    }
  private:
    pin_t rowPins[nb_rows], colPins[nb_cols];
    const uint8_t (&addresses)[nb_rows][nb_cols];
};

This way, I can either initialize it with array reference or with brace-enclosed initializer list.
The only drawback is that when using array references, there's two copies of the same data. The target platform is Arduino, so I'd like to keep memory usage to a minimum.
Is there a way to determine whether an initializer list was used to initialize it, and if so, dynamically memory for the array?
Something along these lines: C++ overload constructor with const array and initializer_list

It would be nice to have compile-time errors if the dimensions of the initializer lists don't match nb_rows and nb_cols, so that rules out std::initializer_list (please correct me if I'm wrong).

8
  • rows and cols you provide to ButtonMatrix are values or a reference? I think this could clarify yourself which way is the right one to follow. Commented Sep 23, 2017 at 17:14
  • addresses being multidimensional severely complicates dynamic allocation. Can this be a flat array of size nb_rows * nb_cols instead? Commented Sep 23, 2017 at 17:16
  • How do you plan to dispose of rowPins and colPins if you do allocate the memory dynamically? Commented Sep 23, 2017 at 17:17
  • @GiuseppePuoti : if I use the first way of initialization (using arrays), they are references to these global arrays, and if I use a brace-enclosed initializer list, they are values that are used to initialize two temporary arrays, which rowPins and colPins are references to. The latter option would require to copy these two temporary arrays. Commented Sep 23, 2017 at 17:24
  • @cdhowie : I could probably live with that. However, it would be easier to use if it's a real 2D matrix. Commented Sep 23, 2017 at 17:26

1 Answer 1

2

If we can replace naked arrays with std::array (they are much easier to dynamically allocate for multidimensional arrays), a partial solution is to rely on the fact that temporaries prefer to be passed as rvalue references:

template <typename T>
void no_deleter(T*) { }

template <typename T>
void default_deleter(T *p) {
    delete p;
}

template <size_t nb_rows, size_t nb_cols>
class ButtonMatrix
{
  public:
    using row_array_t = std::array<pin_t, nb_rows>;
    using col_array_t = std::array<pin_t, nb_cols>;
    using address_array_t = std::array<std::array<uint8_t, nb_cols>, nb_rows>;

  private:
    template <typename T>
    using array_ptr = std::unique_ptr<T, void (*)(T*)>;

  public:
    ButtonMatrix(const row_array_t &rowPins, const col_array_t &colPins, const address_array_t &addresses)
      : rowPins(&rowPins, no_deleter<const row_array_t>),
        colPins(&colPins, no_deleter<const col_array_t>),
        addresses(&addresses, no_deleter<const address_array_t>) { }

    ButtonMatrix(row_array_t &&rowPins, col_array_t &&colPins, address_array_t &&addresses)
      : rowPins(new row_array_t(std::move(rowPins)), default_deleter<const row_array_t>),
        colPins(new col_array_t(std::move(colPins)), default_deleter<const col_array_t>),
        addresses(new address_array_t(std::move(addresses)), default_deleter<const address_array_t>) { }

  private:
    array_ptr<const row_array_t> rowPins;
    array_ptr<const col_array_t> colPins;
    array_ptr<const address_array_t> addresses;
};

Now, when you do this, the passed static arrays get stored by pointer and won't be deleted:

ButtonMatrix<2, 2>::row_array_t rows{{0, 1}};
ButtonMatrix<2, 2>::col_array_t cols{{2, 3}};

ButtonMatrix<2, 2>::address_array_t addresses = {{
  {{0x01, 0x02}},
  {{0x03, 0x04}}
}};

ButtonMatrix<2, 2> bm(rows, cols, addresses);

But, if you pass everything inline, a copy gets made into a heap-allocated array, which does get deleted properly:

ButtonMatrix<2, 2> bm2({{0, 1}}, {{2, 3}}, {{{{1, 2}}, {{3, 4}}}});

(Note you need double braces on everything because of the way the std::array constructor works.)


This is all a bit dangerous though, since temporaries can also bind to a const reference -- if you mix static and temporary data in the same constructor call, the first constructor will be called and you'll store a pointer to a temporary that's going away. Not good.

A better solution (allowing both static and temporary data in the same constructor invocation) will require a lot of template magic. I may try to come up with this, but I may not have time.

Even though it's a bit more verbose, I would go with a solution where you specify at the call site whether the argument must be copied. It will be much safer, and much easier to prove that you aren't storing long-lived pointers to temporaries.

Sign up to request clarification or add additional context in comments.

4 Comments

If memory is at a premium, you might also consider defining a nested class ButtonMatrix<m,n>::Data to hold the three std::array members and put one std::unique_ptr<Data, void(*)(Data*)> in ButtonMatrix, so that a ButtonMatrix object contains just one object pointer and one function pointer, rather than three object pointers and three function pointers.
@aschepler Right. This solution was designed with the idea that some arrays might be dynamically allocated and others not. I haven't implemented that possibility in the constructor, but wanted to leave the data member detail of that solution finished.
Thanks a lot, this is very interesting (and a lot to take in). However, I don't know if it's feasible on an Arduino. There's no support for std::array or std::unique_ptr, for example.
@tttapa It's not too hard to write similar templates, just enough to get this working.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.