24

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
aaragon
  • 2,314
  • 4
  • 26
  • 60
  • As noted, there's nothing more obscure needed than a constructor taking `std::initializer_list< std::initializer_list< T > >`. But it only works with initialization in a declaration, not in an assignment expression, because operators don't work with braced-init-lists. And compiler support for braced-init-lists is still widely buggy, so you might need an upgrade or to try another platform. – Potatoswatter Apr 04 '13 at 12:13

4 Answers4

22

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.

schanq
  • 864
  • 1
  • 15
  • 25
ForEveR
  • 55,233
  • 2
  • 119
  • 133
  • He should probably use vectors instead of array for data too. –  Apr 04 '13 at 11:55
  • 1
    You should note (and for bonus points assert) the preconditions that `lst` isn't empty, and that each row has the same size. – Mike Seymour Apr 04 '13 at 11:59
  • 3
    No point in passing an `initializer_list` by reference. Value would be more appropriate. An `initializer_list` does not own or deep-copy its contents. – Potatoswatter Apr 04 '13 at 12:15
  • @stardust_ why a vector? A vector is just a wrapper on an array as well, right? – aaragon Apr 04 '13 at 13:16
  • @AlejandroMarcosAragon I think this explains it somehow. http://stackoverflow.com/questions/381621/using-arrays-or-stdvectors-in-c-whats-the-performance-gap. And More on google. –  Apr 04 '13 at 13:23
  • Noted, but then again I can't use a vector because the matrix can be also a wrapper to existing memory (in which case the matrix doesn't own the memory). – aaragon Apr 04 '13 at 13:47
  • Is the number of rows in the matrix, but in the general class it doesn't show up anymore. – aaragon Apr 04 '13 at 20:20
  • So the link doesn't work, can you please reupload the example? I tried applying the code to what I have, albeit the header is different, and all it prints out is 1. – SomeStudent Sep 21 '15 at 19:47
  • This doesn't work if the nested `vector` is `const`. :( – Adrian Dec 30 '16 at 00:00
  • I think you don't need `i` and `j`. If you only have a variable `i`, starting from `i = 0`, you can go through all elements by just incrementing it in the inner loop, that is in the inner loop you have: `data[i] = v; ++i` . – Ali Nov 03 '21 at 09:18
3

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

  • 1
    This method is a pain in the butt, but I transited to it for matrices because it has the HUGE advantage of delegating size checks to the compiler. In OP's method (and how I used to have my matrices as well) if you want to be safe you need to always add a size check before every operation, and since operations on matrices are already costly it's not the best. With this method the compiler is gonna tell you straight up "No operator + matches Matrix<2,2> and Matrix<2,3>". That's pretty cool. – Hadron Mar 05 '22 at 17:06
  • Could you explain what `elem_t (&...list)[n]` does? Also, what does `((pos = std::copy(list, list + n, pos)), ...);` do? Lastly, the code will not compile, since `int data[dim...];` is not valid – Richard Robinson Apr 14 '22 at 04:40
  • @RichardRobinson elem_t (&...list)[n] is a variadic list of deducible arrays references. In the above example, there will be 4 arguments of type elem_t (&list)[n] (which is just the reference form for a c-array) passed into the constructor. ((pos = std::copy(list, list + n, pos)), ...); is a one-liner for "copy all of these lists into the data". Each argument is copied and then stores the place it stops copying into pos. The fold expression over the comma operator does this for each argument. I have fixed the compiler error you pointed out, I believe. – Christopher Mauer Apr 18 '22 at 18:09
1

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

Ali
  • 366
  • 4
  • 11
  • 1
    Consider playing with [`std::vector::insert`](https://en.cppreference.com/w/cpp/container/vector/insert) and the `begin()` and `end()` member functions of `std::initializer_list` instead of an inner `push_back` loop. – Bob__ Nov 03 '21 at 09:17
  • @Bob__ Thank you! I updated the answer. If you have any further notes/thoughts on that, please let me know. – Ali Nov 03 '21 at 13:03
  • 1
    Well, a [couple](https://godbolt.org/z/bsE5q5fGW), if I may ;) – Bob__ Nov 03 '21 at 13:41
  • @Bob__ This is awesome! I'll edit the answer. – Ali Nov 03 '21 at 14:52
  • @Bob__ in the last test in `main`, you didn't pass the data in curly braces notation; it printed 4 rows. Is this related to the memory allocation? – Ali Nov 03 '21 at 15:08
  • 1
    That line calls the `Matrix(size_t m, size_t n)` constructor, which allocate *and initialize* (to all zeroes) the vector. – Bob__ Nov 03 '21 at 16:16
  • @Bob__ Yeah exactly! I'm losing my mind. :) – Ali Nov 03 '21 at 16:21
0

i might be a bit late but here is code for generally initializing tensors, regardless if they are matricies or vectors or whatever tensor.You could restrict it by throwing runtime errors when its not a matrix. Below is the source code to extract the data from the initilizer_list its a bit hacky. The whole trick is that the constructor are implicitly called with the correct type.

#include <initializer_list>
#include <iostream>
using namespace std;

class ShapeElem{
public:
    ShapeElem* next;
    int len;

    ShapeElem(int _len,ShapeElem* _next): next(_next),len(_len){}

    void print_shape(){
        if (next != nullptr){
            cout <<" "<< len;
            next->print_shape();
        }else{
            cout << " " <<  len << "\n";
        }
    }

    int array_len(){
        if (next != nullptr){
            return len*next->array_len();
        }else{
            return len;
        } 
    }
};

template<class value_type>
class ArrayInit{
public:
    void* data = nullptr;
    size_t len;
    bool is_final;

    ArrayInit(std::initializer_list<value_type> init) : data((void*)init.begin()), len(init.size()),is_final(true){}

    ArrayInit(std::initializer_list<ArrayInit<value_type>> init): data((void*)init.begin()), len(init.size()),is_final(false){}

    ShapeElem* shape(){
        if(is_final){
            ShapeElem* out = new ShapeElem(len,nullptr);
        }else{
            ArrayInit<value_type>* first = (ArrayInit<value_type>*)data;
            ShapeElem* out = new ShapeElem(len,first->shape());
        }
    }
    void assign(value_type** pointer){
        if(is_final){
            for(size_t k = 0; k < len;k ++ ){
                (*pointer)[k] =  ( ((value_type*)data)[k]);
            }
            (*pointer) = (*pointer) + len;
        }else{
            ArrayInit<value_type>* data_array = (ArrayInit<value_type>*)data;
            for(int k = 0;k < len;k++){
                data_array[k].assign(pointer);
            }
        }
    }
};


int main(){
    auto x = ArrayInit<int>({{1,2,3},{92,1,3}});
    auto shape = x.shape();
    shape->print_shape();
    int* data = new int[shape->array_len()];
    int* running_pointer = data;
    x.assign(&running_pointer);
    for(int i = 0;i < shape->array_len();i++){
        cout << " " << data[i];
    }
    cout << "\n";
}

outputs

 2 3
 1 2 3 92 1 3

The shape() function will return you the shape of the tensor at each dimension. The array is exactly saved as it is written down. It's really import to create something like shape since this will give you the ordering in which the elements are.

If you want a specific index out of the tensor lets say a[1][2][3] the correct position is in 1*a.shape[1]a.shape[2] + 2a.shape[2] + 3

Some minor details and tricks can be found in: https://github.com/martinpflaum/multidimensional_array_cpp

rtuiiop
  • 11
  • 4