Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

is there a way to pass nested initializer lists in C++11 to construct a 2D matrix?

Imagine you have a simple matrix class

template <typename T = double>
class Matrix {

  T* data;
  size_t row, col;

public:

  Matrix(size_t m, size_t n) : row(m), col(n), data(new T[m*n]) {}
  //...       

  friend std::ostream& operator<<(std::ostream& os, const Matrix& m) {
    for (int i=0; i<m.row; ++i) {
      for (int j=0; j<m.col; ++j)
        os<<" "<<m.data[i + j*m.row];
      os<<endl;
    }
    return os;
  }
};      

Is there a way that I can initialize this matrix with an initializer list? I mean to obtain the sizes of the matrix and the elements from an initializer list. Something like the following code:

Matrix m = { {1., 3., 4.}, {2., 6, 2.}};

would print

 1 3 4
 2 6 2

Looking forward to your answers. Thank you all. aa

EDIT

So I worked on your suggestions to craft a somewhat generic array that initializes elements using initializer lists. But this is the most generic I could obtain. I would appreciate if any of you have any suggestions as to make it a more generic class. Also, a couple of questions:

  • Is it fine that a derived class initializes the state of the base class? I'm not calling the base constructor because of this, but should I call it anyways?
  • I defined the destructor a the Generic_base class as protected, is this the right way to do it?
  • Is there any foreseeable way to carry out the code that belongs to the constructor of the initializer in a more generic way? I mean to have one general constructor that takes care of all cases?

I included just the necessary code to illustrate the use of initializer lists in construction. When going to higher dimensions it gets messy, but I did one just to check the code.

#include <iostream>
#include <cassert>

using std::cout;
using std::endl;


template <int d, typename T>
class Generic_base {

protected:

  typedef T value_type;

  Generic_base() : n_(), data_(nullptr){}

  size_t n_[d] = {0};
  value_type* data_;
};



template <int d, typename T>
class Generic_traits;


template <typename T>
class Generic_traits<1,T> : public Generic_base<1,T> {

protected:

  typedef T value_type;
  typedef Generic_base<1,T> base_type;
  typedef std::initializer_list<T> initializer_type;

  using base_type::n_;
  using base_type::data_;


public:

  Generic_traits(initializer_type l) {

    assert(l.size() > 0);
    n_[0] = l.size();
    data_ = new T[n_[0]];

    int i = 0;
    for (const auto& v : l)
      data_[i++] = v;
  }
};


template <typename T>
class Generic_traits<2,T> : public Generic_base<2,T> {

protected:

  typedef T value_type;
  typedef Generic_base<2,T> base_type;

  typedef std::initializer_list<T> list_type;
  typedef std::initializer_list<list_type> initializer_type;

  using base_type::n_;
  using base_type::data_;

public:

  Generic_traits(initializer_type l) {

    assert(l.size() > 0);
    n_[0] = l.size();
    n_[1] = l.begin()->size();

    data_ = new T[n_[0]*n_[1]];

    int i = 0, j = 0;
    for (const auto& r : l) {

      assert(r.size() == n_[1]);
      for (const auto& v : r) {
        data_[i + j*n_[0]] = v;
        ++j;
      }
      j = 0;
      ++i;
    }
  }
};


template <typename T>
class Generic_traits<4,T> : public Generic_base<4,T> {

protected:

  typedef T value_type;
  typedef Generic_base<4,T> base_type;

  typedef std::initializer_list<T> list_type;
  typedef std::initializer_list<list_type> llist_type;
  typedef std::initializer_list<llist_type> lllist_type;
  typedef std::initializer_list<lllist_type> initializer_type;

  using base_type::n_;
  using base_type::data_;

public:

  Generic_traits(initializer_type l) {

    assert(l.size() > 0);
    assert(l.begin()->size() > 0);
    assert(l.begin()->begin()->size() > 0);
    assert(l.begin()->begin()->begin()->size() > 0);

    size_t m = n_[0] = l.size();
    size_t n = n_[1] = l.begin()->size();
    size_t o = n_[2] = l.begin()->begin()->size();
    n_[3] = l.begin()->begin()->begin()->size();

    data_ = new T[m*n*o*n_[3]];

    int i=0, j=0, k=0, p=0;
    for (const auto& u : l) {
      assert(u.size() == n_[1]);
      for (const auto& v : u) {
        assert(v.size() == n_[2]);
        for (const auto& x : v) {
          assert(x.size() == n_[3]);
          for (const auto& y : x) {
            data_[i + m*j + m*n*k + m*n*o*p] = y;
            ++p;
          }
          p = 0;
          ++k;
        }
        k = 0;
        ++j;
      }
      j = 0;
      ++i;
    }
  }
};



template <int d, typename T>
class Generic : public Generic_traits<d,T> {

public:

  typedef Generic_traits<d,T> traits_type;
  typedef typename traits_type::base_type base_type;

  using base_type::n_;
  using base_type::data_;

  typedef typename traits_type::initializer_type initializer_type;

  // initializer list constructor
  Generic(initializer_type l) : traits_type(l) {}

  size_t size() const {
    size_t n = 1;
    for (size_t i=0; i<d; ++i)
      n *= n_[i];
    return n;
  }

  friend std::ostream& operator<<(std::ostream& os, const Generic& a) {
    for (int i=0; i<a.size(); ++i)
      os<<" "<<a.data_[i];
    return os<<endl;
  }      
};


int main()
{

  // constructors for initializer lists

  Generic<1, double> y = { 1., 2., 3., 4.};
  cout<<"y -> "<<y<<endl;

  Generic<2, double> C = { {1., 2., 3.}, {4., 5., 6.} };
  cout<<"C -> "<<C<<endl;

  Generic<4, double> TT = { {{{1.}, {7.}, {13.}, {19}}, {{2}, {8}, {14}, {20}}, {{3}, {9}, {15}, {21}}}, {{{4.}, {10}, {16}, {22}}, {{5}, {11}, {17}, {23}}, {{6}, {12}, {18}, {24}}} };
  cout<<"TT -> "<<TT<<endl;

  return 0;
}

Which prints as expected:

y ->  1 2 3 4

C ->  1 4 2 5 3 6

TT ->  1 4 2 5 3 6 7 10 8 11 9 12 13 16 14 17 15 18 19 22 20 23 21 24
like image 935
aaragon Avatar asked Apr 04 '13 11:04

aaragon


People also ask

What is std :: Initializer_list?

An object of type std::initializer_list<T> is a lightweight proxy object that provides access to an array of objects of type const T .

Does order of initializer list matter?

In the initializer list, the order of execution takes place according to the order of declaration of member variables. While using the initializer list for a class in C++, the order of declaration of member variables affects the output of the program.

Is initialization list faster?

Conclusion: All other things being equal, your code will run faster if you use initialization lists rather than assignment. Note: There is no performance difference if the type of x_ is some built-in/intrinsic type, such as int or char* or float .

What is brace initialization?

If a type has a default constructor, either implicitly or explicitly declared, you can use brace initialization with empty braces to invoke it. For example, the following class may be initialized by using both empty and non-empty brace initialization: C++ Copy.


3 Answers

Why not?

  Matrix(std::initializer_list<std::initializer_list<T>> lst) :
  Matrix(lst.size(), lst.size() ? lst.begin()->size() : 0)
  {
     int i = 0, j = 0;
     for (const auto& l : lst)
     {
        for (const auto& v : l)
        {
           data[i + j * row] = v;
           ++j;
        }
        j = 0;
        ++i;
     }
  }

And as stardust_ suggests - you should use vectors, not arrays here.

like image 138
ForEveR Avatar answered Oct 29 '22 17:10

ForEveR


The main issue with using initializer lists to tackle this problem, is that their size is not easily accessible at compile time. It looks like this particular class is for dynamic matrices, but if you wanted to do this on the stack (usually for speed/locality reasons), here is a hint at what you need (C++17):

template<typename elem_t, std::size_t ... dim>
struct matrix
{
    template<std::size_t ... n>
    constexpr matrix(const elem_t (&...list)[n]) : data{}
    {
        auto pos = &data[0];
        ((pos = std::copy(list, list + n, pos)), ...);
    }

    elem_t data[(dim * ... * 1)];
};

template<typename ... elem_t, std::size_t ... n>
matrix(const elem_t (&...list)[n]) -> matrix<std::common_type_t<elem_t...>, sizeof...(n), (n * ... * 1) / sizeof...(n)>;

I had to tackle this same problem in my linear algebra library, so I understand how unintuitive this is at first. But if you instead pass a C-array into your constructor, you will have both type and size information of the values you've passed in. Also take note of the constuctor template argument deduction (CTAD) to abstract away the template arguments.

You can then create constexpr matrix objects like this (or, leave out constexpr to simply do this at runtime on the stack):

constexpr matrix mat{ {1, 2, 3}, {4, 5, 6}, {7, 8, 9}, {10, 11, 12} };

Which will initialize an object at compile time of type:

const matrix<int, 4, 3>

If C++20 is supported by your compiler, I would recommend adding a "requires" clause to the CTAD to ensure that all sub-arrays are the same size (mathematically-speaking, n1 == n2 == n3 == n4, etc).

like image 37
Christopher Mauer Avatar answered Oct 29 '22 17:10

Christopher Mauer


Using std::vector::emplace_back() (longer)

Using std::vector, instead of plain old array, you can use std::vector::emplace_back() to fill the vector:

template <typename T = double>
class Matrix {
    std::vector<T> data;
    size_t row{}, col{}; // Non-static member initialization

public:
    Matrix(size_t m, size_t n) : data(std::vector<T>(m * n)), row(m), col(n)
    { //                         ^ Keep the order in which the members are declared
    }
    Matrix(std::initializer_list<std::initializer_list<T>> lst)
        : row(lst.size())
        , col(lst.size() ? lst.begin()->size() : 0) // Minimal validation
    {
        // Eliminate reallocations as we already know the size of matrix
        data.reserve(row * col);
        for (auto const& r : lst) {
            for (auto const &c : r) {
                data.emplace_back(c);
            }
        }
    }
};

int main() {
    Matrix<double> d = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
}

Using std::vector::insert() (better and shorter)

As @Bob mentioned in a comment, you can use std::vector::insert() member function, instead of the inner emplace_back loop:

template <typename T = double>
class Matrix {
    std::vector<T> data;
    size_t row{}, col{}; // Non-static member initialization

public:
    Matrix(size_t m, size_t n) : data(std::vector<T>(m * n)), row(m), col(n)
    { //                         ^ Keep the order in which the members are declared
    }
    Matrix(std::initializer_list<std::initializer_list<T>> lst)
        : row{lst.size()}
        , col{lst.size() ? lst.begin()->size() : 0} // Minimal validation
    {
        // Eliminate reallocations as we already know the size of the matrix
        data.reserve(row * col);
        for (auto const& r : lst) {
            data.insert(data.end(), r.begin(), r.end());
        }
    }
};

int main() {
    Matrix<double> d = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
}

So, we're saying: For each row (r) in the lst, insert the content of the row from the beginning (r.begin()) to the end (r.end()) into the end of the empty vector, data, (in an empty vector semantically we have: empty_vec.begin() == empty_vec.end()).

like image 40
Ali Avatar answered Oct 29 '22 16:10

Ali