简体   繁体   中英

Template class with variable array size: initialize with array reference or brace-enclosed initializer list

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).

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM