Dynamic table (like a 2d array, but with vectors): How to increment size (like push_back)?

Time appropriate greetings!

For something that I am working on I need a dynamic table of objects.
(So like a 2D array, but with vectors)
Because I don't know how many rows or columns are needed.
(Probably more columns then rows, maybe like a million rows and a hand full of columns, but I don't know yet ...)
A onedimensional vector is no problem, I know how to work with those.
(Use push_back(1) to append an element and stuff like that ...)

What I want to do is to have like a table with rows and columns.
Each "cell" of that table is an object (all of the same class).

How can I "add" one row or one column to that "table of objects" dynamically at runtime?

This is just an example of what I am trying to do:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//The class of all objects in the table:
class Cell
{
  int SomeValue;
  int AnotherValue;
  float RandomValue;
  string AndAnotherRandomValue;
  //Lots of other variables are here, most are integers)
}

//The "vector of vectors" (= 2D vector, right?)
vector < vector < Cell > > DynamicTable;

//A function that should work like push_back(1) on the rows of the table
void AddRowToDynamicTable()
{
  //What do I need to put here?
}


//A function that should work like push_back(1) on the columns of the table
void AddColumnToDynamicTable()
{
  //What do I need to put here?
}


And "reading / writing" stuff to each object in this "table" should work like this, right?
(After the 2D vector table has been resized to include those row & col values, of course)

1
2
3
4
5
6

//Reading the variable SomeValue from the object at row 10 column 24:
cout << DynamicTable[10,24].SomeValue;

//Writing into the variable RandomValue of the object at row 5 column 1:
DynamicTable[5,1].RandomValue = 9.852;


Right?
Or am I interpreting something wrong here?
If so, please let me know ...

Also, since this is quite a bit of data, should I consider writing and reading this stuff into and from a txt file, so that that RAM doesn't "run out"?
That would slow the programm down a lot, but that isn't really something that I care about, as long as it works.
Last edited on
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
#include <iostream>
#include <vector>

// overloaded operator<< to display a 2D vector
template <typename T>
std::ostream& operator<<(std::ostream&, const std::vector<std::vector<T>>&);

// overloaded operator<< to display a 1D vector
template <typename T>
std::ostream& operator<<(std::ostream&, const std::vector<T>&);

int main()
{
   // let's create an empty 2D vector
   std::vector<std::vector<int>> vec;

   // let's populate the vector, 4 x 5
   int start { };

   for (int row { }; row < 4; ++row)
   {
      // create a temp row vector to populate with the needed columns
      std::vector<int> temp;

      for (int col { }; col < 5; ++col)
      {
         temp.push_back((((row + 1) * 100) + start) + (col + 1));
      }

      vec.push_back(temp);
   }

   // let's display the vector
   for (const auto& row : vec)
   {
      for (const auto& col : row)
      {
         std::cout << col << ' ';
      }

      std::cout << '\n';
   }
   std::cout << '\n';

   // let's add a couple of values
   vec[1].push_back(999);
   vec[1].insert(vec[1].begin() + 2, 888);

   std::cout << vec << '\n';

   // let's erase a couple of values
   for (auto& row : vec)
   {
      for (auto itr = row.begin(); itr < row.end();)
      {
         if (*itr == 303)
         {
            itr = row.erase(itr);
         }
         else
         {
            ++itr;
         }
      }
   }

   vec[1].pop_back();

   std::cout << vec;
}

template <typename T>
std::ostream& operator<<(std::ostream& os, const std::vector<std::vector<T>>& v)
{
   for (auto const& x : v)
   {
      os << x << '\n';
   }

   return os;
}

template <typename T>
std::ostream& operator<<(std::ostream& os, const std::vector<T>& v)
{
   for (auto const& x : v)
   {
      os << x << ' ';
   }

   return os;
}
//Reading the variable SomeValue from the object at row 10 column 24:
cout << DynamicTable[10,24].SomeValue;


You would need to use:

1
2
//Reading the variable SomeValue from the object at row 10 column 24:
cout << DynamicTable[10][24].SomeValue;


You could also provide an operator() so that you could write:

1
2
//Reading the variable SomeValue from the object at row 10 column 24:
cout << DynamicTable(10,24).SomeValue;


Using [a,b] format isn't yet supported in C++ - its coming in a future version. What this actually means is execute the expression a then the expression b and use the result of expression b as index to the array. The comma operator! This format is a deprecated feature in C++20. You'd use the comma operator in this situation as:

1
2
//Reading the variable SomeValue from the object at row 10 column 24:
cout << DynamicTable[(10,24)].SomeValue;


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <iostream>
#include <vector>

struct cell
{
    // cell() = default ;

    int someValue = 0 ;
    // ....
};


using row_type = std::vector<cell> ; // a row is a vector of cells
using table_type = std::vector<row_type> ; // table is a collection of rows

void add_column( table_type& table ) // add a column to every row of the table
{
    for( row_type& row : table ) // for each row in the table
        row.push_back( {} ) ; // add a column (push back a default constructed cell)
}

void add_row( table_type& table ) // add a row to the table
{
    // if the table is empty, add an empty row (we have no idea how many columns should be there)
    if( table.empty() ) table.push_back( {} ) ; // push back a default constructed vector

    // otherwise add a new row which has the same number of columns as the last row
    else table.push_back( row_type( table.back().size() ) ;
}

int main()
{
    table_type DynamicTable ; // create an empty table

    // make it 30x100
    for( int i = 0 ; i < 5 ; ++i ) add_row(DynamicTable) ; // add 5 empty rows
    for( int i = 0 ; i < 100 ; ++i ) add_column(DynamicTable) ; // to each row, add 100 columns
    for( int i = 0 ; i < 25 ; ++i ) add_row(DynamicTable) ; // add 25 more rows, each having 100 columns

    // Reading the variable SomeValue from the object at row 10 column 24:
    // std::cout << DynamicTable[10,24].SomeValue;
    std::cout << DynamicTable[10][24].SomeValue << '\n' ;
    // DynamicTable[10] // get to the vector at row number 10
    // DynamicTable[10][24] // get to the cell at column 24 of that vector
}
Thanks for all the quick replies!

The one from JLBorges made the most sense to me.
(Besides the "using ..." - namespace stuff, but that might just be my own ignorance of that subject so far ...)

Also, I think a struct will work instead of using a class and objects.
(Why overcomplicate things, right?)

"A row is a vector of cells" and "a table is a collection of rows"

That really makes sense and is a great way to look at a "2d" vector "array".

So basically a table is a vector of vectors of cells.


Also, thanks seeplus for clarifying that about the [a,b] and [a][b] thing.
That's why I was getting errors everytime I tried [a,b]!
Ok, so I just tried to understand in detail how this works ...

This is the function to add a row to the "table", from JLBorges's code:

1
2
3
4
5
void add_column( table_type& table ) // add a column to every row of the table
{
    for( row_type& row : table ) // for each row in the table
        row.push_back( {} ) ; // add a column (push back a default constructed cell)
}


The table_type& is clearly used to tell the function what type this parameter is, like int or char or whatever.
But that for-loop doesn't make any sense to me.
How does this function even "know" about something called "row"?
There is no global variable like that and the only parameter is called "table".

This is kind of like "magic" to me.
It clearly works, but I don't know how ...

(I want to modify that function to only add a column to a specified row, row number given as a parameter)
That for loop is a "range based for loop," from C++11. AKA a "for-each loop."

https://www.learncpp.com/cpp-tutorial/6-12a-for-each-loops/

It is some very useful C++11 "magic."
(I want to modify that function to only add a column to a specified row, row number given as a parameter)

Line 46 in my code example, pushes back a new element (adds a new column) to a specified row. Line 47, with an iterator you can insert a new column at a location other than the end.
Ok, so I that page about the "for each" loop and it makes sense ...

But I still don't know how that function that I mentioned previously "knows" about "row".
Like I said, it isn't a parameter or a global ....

About the "pushback in individual row":
Ok, so I can just use "vec[1].push_back(999);" to access (and push_back) a single row!
I thought I would always have to use vec[a][b] when doing stuff ...
(But actually this makes sense, because it is a vector of vectors, not really a "2D array")

@Furry Guy:
I like your username, by the way ;-)
vec[1].push_back(999); pushes back a single element to a vector at its end. That is the second row vector stored in the 2D vector.

A 2D vector is a vector of vectors. Each "row" contains a 1D vector that represents all of that row's columns.
Range-based for loops make "walking through" any of the C++ standard library containers, 1D or 2D, std::vector or std::list, etc., much easier.

Printing out a 2D vector with range-based loops (from my code example earlier):
33
34
35
36
37
38
39
40
41
42
43
   // let's display the vector
   for (const auto& row : vec)
   {
      for (const auto& col : row)
      {
         std::cout << col << ' ';
      }

      std::cout << '\n';
   }
   std::cout << '\n';


You don't need to know the sizes of any of the vectors, the compiler handles the details.

With a 2D vector each row doesn't have to contain the same number of columns.

My example shows possible ways to handle "on demand" insertion/deletion of elements.

Another advantage of a C++ standard library container: when you pass it to a function, vs. passing a regular array, The C++ standard library container "remembers" its size so you don't need to have an additional parameter that passes the container's size. A C++ standard library container doesn't devolve to a pointer when passed to a function, like a regular array does.

If you need to manipulate an individual element, not add or erase one, is when you could use the vector's operator[] ([][] with 2D, [][][] with 3D).

std::vector has another method for element access, the .at() member function. vec.at(1).at(3) 2nd row, 4 column for example.
https://en.cppreference.com/w/cpp/container/vector/at
Ok, but why is this "col" there?
It isn't a global and the only parameter is called "row":
1
2
3
4
5
6
7
8
9
10
11
  // let's display the vector
   for (const auto& row : vec)
   {
      for (const auto& col : row)
      {
         std::cout << col << ' ';
      }

      std::cout << '\n';
   }
   std::cout << '\n';


How does this function "know" about this variable "col"?
There should be an error that this variable isn't declared anywhere, right?
Or am I missing something here?
The first loop is using the row variable as the element variable for the outer vector. The rows vector.

The second loop is using the col variable as the element variable for the inner sets of vectors. The individual vector contained in each row element.

Remember: a 2D vector is a vector (rows) of vectors (columns).

I simply uses descriptive variable names so they self-document what each loop is iterating through. row for each row in the 2D vector, col for each column contained in each row vector.

Each element variable in the for-each loops is being created in the body of the loop declaration using auto.
https://www.learncpp.com/cpp-tutorial/the-auto-keyword/

Actually const auto& which tells the compiler to:

1. const so the vector can't be changed in any way, making it read-only.

2. not create a copy of the vector(s), use the actual vector itself by using a reference (&).

If you used the old fashioned style of loops to iterate through a 2D vector it might be something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  // let's display the vector

   // vec.size() returns the number of rows
   for (size_t row { }; row < vec.size(); ++row)
   {
      // vec[row].size() returns the number of columns in each row vector
      for (size_t col { }; col < vec.[row].size(); ++col)
      {
         std::cout << vec[row][col] << ' ';
      }

      std::cout << '\n';
   }
   std::cout << '\n';

Very similar to the way you'd iterate through a regular 2D array.
Oh!
Now I get it!
That variable is defined withing the for(xxx) "statement".
Just like the variable i is here: for(int i = 0, .....)

I just didn't "recognize" it, becaue I am not used to this weird for loop stuff ...

Actually it makes sense!

Sorry, I was just a little bit confused.

And I don't like using pieces of program that I don't understand. I like to know EXACTLY what the program is doing and how it is doing it. I don't just "trust" a solution because "it works".
I know "copy & paste" "programming" is really popular these days, but I'd like to know WHY and HOW it works the way it works.
(That's one reason why I got into this)
There are several different ways to iterate through a std::vector. Take this example:
std::vector<int> vec { 1, 2, 3, 4, 5 };

A regular for loop, using std::vector's operator[] to access the elements:
1
2
3
4
   for (size_t i { }; i < vec.size(); ++i)
   {
      std::cout << vec[i] << ' ';
   }


C++ standard library containers can use iterators, a pointer-like object, for accessing the contents of the container:
1
2
3
4
   for (std::vector<int>::iterator itr { vec.begin() }; itr != vec.end(); ++itr)
   {
      std::cout << *itr << ' ';
   }

Because an iterator is like a pointer you have to dereference the loop variable to get the element's value in each iteration.

A range based for loop uses iterators in a somewhat compact format:
1
2
3
4
   for (std::vector<int>::iterator::value_type itr : vec)
   {
      std::cout << itr << ' ';
   }

Dereferencing the loop variable is already done by specifying the vector's value_type; the loop variable in each iteration contains a copy of the element's value.

You could specify the vector's contained type (int) instead:
1
2
3
4
   for (int itr : vec)
   {
      std::cout << itr << ' ';
   }


I personally prefer to let the compiler deduce the type:
1
2
3
4
   for (auto itr : vec)
   {
      std::cout << itr << ' ';
   }


Using auto means I can use the same code for different types. Doubles, std::strings, etc.

Instead of creating a copy of the container when iterating I prefer to specify a reference when creating the range-based for loop:
1
2
3
4
   for (auto& itr : vec)
   {
      std::cout << itr << ' ';
   }

Now I can modify the container's elements if I wanted.
1
2
3
4
   for (auto& itr : vec)
   {
      itr += 2;
   }


If I don't want to modify the elements:
1
2
3
4
   for (const auto& itr : vec)
   {
      std::cout << itr << ' ';
   }


C++11 made a lot of changes to the language that help writing better code with less hassle. A range-based for loop is one of those changes.
Last edited on
Topic archived. No new replies allowed.