How to have a class array with non-static, const dimensions?

closed account (Ezyq4iN6)
What I'm interested in is creating a class that defines two consts and creates a 2D array with these. The catch is that I don't want to use static for the consts, because I want to create different instances. Please see my example.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Map_Maker.hpp
#ifndef MAP_MAKER_HPP
#define MAP_MAKER_HPP

class Map_Maker
{
  const int ROWS;
  const int COLUMNS;
  int map_layout[ROWS][COLUMNS];

  public:
    Map_Maker(int rows, int cols):ROWS(rows), COLUMNS(cols){}
};

#endif

// main.cpp
int main()
{
  Map_Maker map1(10, 10);
  Map_Maker map2(100, 100);

  return 0;
}


Edit #1 (Changed text):
The error I get says "invalid use of non-static data member 'Map_Maker::ROW'. Is there a workaround that allows me to avoid the use of static?

Edit #1 (Added this):
I noticed that even removing the const still causes the compiler to produce the same error. However, adding static produces the error, "array bound is not an integer constant before ']' token".
Last edited on
Unless you want to just use vectors, you probably have to use templates here.

The reason being that if you declare a class that has an array of size 10, the compiler needs to know the size of that particular class at compile time (at least sizeof(int) * 10).
You can't then have that same class have an array of size 20, it needs to be a different class.

In other words, sizeof(Map_Maker) can't be both 10*10*sizeof(int) and 100*100*sizeof(int) at the same time. They need to be two different classes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Example program
#include <iostream>

template <int Rows, int Columns>
class Map_Maker
{
  int map_layout[Rows][Columns];

  public:
    Map_Maker()
    : map_layout{}
    { }
};

int main()
{
  Map_Maker<10, 10> map1;
  Map_Maker<100, 100> map2;
  
  std::cout << sizeof(Map_Maker<10, 10>) << '\n';
  std::cout << sizeof(Map_Maker<100, 100>) << '\n';

  return 0;
}


If you use vectors, you don't need to worry about this, because both instances can be the same class (the vector is dynamically allocated under the hood). But the choice is yours.
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
// Example program
#include <iostream>
#include <vector>

class Map_Maker
{
  std::vector<std::vector<int>> map_layout;

  public:
    Map_Maker(int rows, int cols)
    : map_layout(rows, std::vector<int>(cols)),
      rows(rows),
      columns(cols)
    { }
    
  int rows;
  int columns;
};


int main()
{
  Map_Maker map1(10, 10);
  Map_Maker map2(100, 100);

  return 0;
}

The other advantage of vectors is that you don't need to know how big the map will be at compile-time (what if you want to load a map from a file?).
Last edited on
closed account (E0p9LyTq)
@Ganado, how are you going to access the individual elements in your map class when using vectors? Without overloading operator[].

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
#include <iostream>
#include <vector>

class Map_Maker
{
private:
   std::vector<std::vector<int>> map_layout;

public:
   Map_Maker(int rows, int cols)
      : map_layout(rows, std::vector<int>(cols))
   {}

   std::vector<std::vector<int>>& GetLayout()
   {
      return map_layout;
   }
};


int main()
{
   Map_Maker map(5, 8);

   std::cout << "Number of rows: " << map.GetLayout().size() << '\n';
   std::cout << "Number of cols: " << map.GetLayout()[0].size() << "\n\n";

   std::cout << map.GetLayout()[0][0] << '\n';

   map.GetLayout()[1][1] = 12;

   std::cout << map.GetLayout()[1][1] << '\n';

   std::cout << "\nmap1:\n";
   for (size_t i = 0; i < map.GetLayout().size(); i++)
   {
      for (size_t j = 0; j < map.GetLayout()[i].size(); j++)
      {
         std::cout << map.GetLayout()[i][j] << ' ';
      }
      std::cout << '\n';
   }
}
Number of rows: 5
Number of cols: 8

0
12

map1:
0 0 0 0 0 0 0 0
0 12 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
Last edited on
FurryGuy wrote:
how are you going to access the individual elements in your map class when using vectors
The same way the original post was going to access the array elements!

(The point being, there was none to begin with. I didn't bother because I didn't see that as being related to the actual question. But your answer does make it more complete, though I would just use the [] operator.)
Last edited on
closed account (E0p9LyTq)
The same way the original post was going to access the array elements!

The access method was being ignored trying to get the 2D array working in the class. The likely next question would be how to access the individual elements when the original problem was solved.

I didn't bother because I didn't see that as being related to the actual question

I anticipated the possible follow-up question, that's all. Using either a template or a vector cripples the class. It does nothing other than be created or implicitly destructed when it goes out of scope.
closed account (Ezyq4iN6)
@Ganado

Thanks for the quick reply.

I think I understand what you mean about how the array will not be able to be redefined, because the compiler wants to go ahead and assign it dimensions at compile time. To compensate, I switched from arrays to vectors per your suggestion.

However, upon doing this I ran into another issue. I wanted to go ahead and set the vector to a certain size, but the compiler has given me a long warning:

warning: narrowing conversion of '((Map_Maker*)this)->Map_Maker::ROW' from 'int' to 'std::vector<std::vector<double> >::size_type {aka unsigned int}' inside { } [-Wnarrowing]|

 
  std::vector<std::vector<double>> sample = {ROW, std::vector<double>(COL)};


This appears to be how I see it on several examples online. While it is only a "warning", I still would like to know what is wrong and how I correct it.
You have ROW and COL as ints? make them size_t instead. That should fix it. Using the { } syntax is more strict, which can be good for detecting some compile-time issues, but doesn't really have an advantage here, imo (if you pass in -3 as the size, the end result either way you're going to have a bad time).

Or use () instead of {} for the constructor/initializer list.

Or, by calling the constructor explicitly (if that's a class member),
std::vector<std::vector<double>> sample = std::vector<std::vector<double>>(ROW, std::vector<double>(COL));
Last edited on
closed account (Ezyq4iN6)
@FurryGuy

Thanks for adding additional details. You are right that I wanted to do more with my program, and I wished I had posted a larger example, but didn't want to clutter it too much.

I actually ended up adding the passing of a 2D vector from main as a reference, so that anything done to it in the object would be available to me without needing to return a vector. The problem is that I am getting warnings and they have messed up my code like an error. In other words, I'm not getting the right data now.

1
2
3
4
5
6
7
8
9
10
11
12
// In .hpp file
Map_Maker(int rows, int cols, std::vector<std::vector<int>> &my_map):
ROW{row}, COL{col}, map_layout{my_map}

// I also added 
std::vector<std::vector<int>> &map_layout;
std::vector<std::vector<double>> sample = {ROW, std::vector<double>(COL)};

// In main, the test_map is filled with integer values.
std::vector<std::vector<int>> test_map = {...};
Map_Maker map1(10, 10, test_map);


I added the sample map to act as a map over the map_layout, which is also of the same dimensions. I have also tried filling it with an initial value by adding ", 0.0" after "COL", but I got a similar warning and it is acting funny in my code when it wasn't before as an array.
In the C++ standard library there’s also std::array, which
encapsulates fixed size arrays
https://en.cppreference.com/w/cpp/container/array
If you don't plan to use it polymorphically, you could risk deriving your class from that, for example:
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
#include <array>
#include <iostream>


// Not polymorphic
template <typename T, std::size_t Howmany>
class MapMaker : public std::array<T, Howmany> {
};


constexpr std::size_t Rows { 3 };
constexpr std::size_t Cols { 4 };


int main()
{
    MapMaker<std::array<double, Cols>, Rows> mm;

    for(std::size_t i {}; i < Rows * Cols; ++i) {
        mm.at(i / (Rows + 1)).at(i % Cols) = static_cast<double>(i + 1);
    }
    
    for(std::size_t i {}; i < Rows; ++i) {
        for(std::size_t j {}; j < Cols; ++j) {
            std::cout << "mm.at(" << i << ").at(" << j << "): "
                      << mm.at(i).at(j) << ' ';
        }
        std::cout << '\n';
    }
    std::cout << '\n';
}


Output:
mm.at(0).at(0): 1 mm.at(0).at(1): 2 mm.at(0).at(2): 3 mm.at(0).at(3): 4
mm.at(1).at(0): 5 mm.at(1).at(1): 6 mm.at(1).at(2): 7 mm.at(1).at(3): 8
mm.at(2).at(0): 9 mm.at(2).at(1): 10 mm.at(2).at(2): 11 mm.at(2).at(3): 12


- - -
the compiler has given me a long warning:
...
[std::vector<std::vector<double>> sample = {ROW, std::vector<double>(COL)};

Since std::vectors provide an initializer_list constructor
https://en.cppreference.com/w/cpp/container/vector/vector
there’s a difference between rounded and curly parentheses:
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
#include <iostream>
#include <vector>


int main()
{
    // std::vector of std::vector of doubles.
    // std::vector will contain 3 std::vector<double>.
    // Each std::vector<double> will contain 1 double set to 6.
    std::vector<std::vector<double>> sample_1 { 3, std::vector<double> { 6 } };

    std::cout << "sample_1:\n";
    for(std::size_t i {}; i < sample_1.size(); ++i) {
        for(std::size_t j {}; j < sample_1.at(i).size(); ++j) {
            std::cout << sample_1.at(i).at(j) << ' ';
        }
        std::cout << '\n';
    }
    std::cout << '\n';

    // std::vector of std::vector of doubles.
    // std::vector will contain 3 std::vector<double>.
    // Each std::vector<double> will contain 6 doubles, all set to 0.0.
    std::vector<std::vector<double>> sample_2 (3, std::vector<double>(6) );

    std::cout << "sample_2:\n";
    for(std::size_t i {}; i < sample_2.size(); ++i) {
        for(std::size_t j {}; j < sample_2.at(i).size(); ++j) {
            std::cout << sample_2.at(i).at(j) << ' ';
        }
        std::cout << '\n';
    }
    std::cout << '\n';
}


Output:
sample_1:
6
6
6

sample_2:
0 0 0 0 0 0
0 0 0 0 0 0
0 0 0 0 0 0

Last edited on
closed account (Ezyq4iN6)
@Ganado

I actually changed them all to unsigned ints before I read your post. I know we discussed the size_t issue before, but can it cause problems if I'm using it in math with other integers, both signed and unsigned, later on? I'm simply concerned that since I don't know its bit size, that I might use it later on down the road and it would come back to haunt me. But I'm new to programming, so I'm open to using whatever is considered best practices.

What is the advantage/disadvantage to {} vs. () in the initializer list? I only used {} because I read that it was the accepted standard now and that it could keep you out of a few problems.

I tried the vector with the equal sign, but it didn't seem to make a difference. Changing things to unsigned ints did though.
closed account (Ezyq4iN6)
@Enoizat

That array option is an interesting approach. At my level of development, I'm not really making a whole lot of programs large enough to justify polymorphism. Most what I make just contains base classes and a main. I might play around with that idea some.

In your second example, I want to definitely produce something like the second option, because I want to pre-select the size of my vector. Though I am using a vector, I don't intended to change the size. It was more of a choice to deal with the resizing issue I was facing using arrays in the first example I posted.
To be honest, I don't use the { } syntax very often, I usually either go with { } for simple structs, or use ( ), so I'm not the best guy to respond for that...

But the issue is that using { } vs ( ) is that { } will never implicitly truncate a narrowing cast (or at least will warn you). An example of a narrowing cast is casting a signed int into an unsigned int (or vice versa).
1
2
3
4
5
int main()
{
    unsigned int a = 42;
    int b{a};
}

 warning: narrowing conversion of 'a' from 'unsigned int' to 'int' inside { } [-Wnarrowing]

So, if you want to use the newer, safer { } syntax, you need to make sure your type aren't being implicitly narrowed to another types. But it still won't prevent you from passing -5 in to a function that expects an unsigned int, because that cast will still be implicit. A lot of type casting in C++ can cause sometimes hard-to-spot bugs.
Last edited on
closed account (Ezyq4iN6)
@Ganado
I did some experimentation with the {} vs (), and I can see the advantage to {}, especially when using unsigned ints.

As for the passing of a negative to a function with an unsigned, I did some experimenting and found out that you can cause it to create a warning by doing the following.

1
2
3
4
5
6
7
8
9
10
11
12

void f(unsigned int a)
{
}

int main()
{
int b = -5;
f({b});
}



It will even cause an error if the 5 is positive, since it is marked as an int, not an unsigned int. I'm glad the {} work in this case too.
The uniform initializer should in general be the preferred type of initialization, not only to avoid narrowing:
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#es23-prefer-the--initializer-syntax
closed account (Ezyq4iN6)
@Enoizat

Even after the link, I'm a bit murky on exactly when to use such things. Are we basically replacing the '=' sign with '{}' around the value/formula, and putting '{}' around all values passed to functions or when creating struct/class instances? Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

// Assignment
int a = 5;
// becomes
int a {5};

// Equation
b = cos(j) / 12.0 * f(y, z);
// becomes
b {cos({j}) / 12.0 * f({y}, {z})};

// New Class
MyClass class1(1, 'a', 3.0);
// becomes
MyClass class1({1}, {'a'}, {3.0}}
Last edited on
I don’t think I would be able to dispel all your doubts, opisop, because, for what I can see, our approach to study is a bit different: you appear to appreciate delving into the details of theory, while I prefer to know the jist of the things in practice. Plus, my English is not rich enough to let me discuss all the ins and outs of anything. But, if you surf internet by the keywords “c++ uniform initialization” or “c++ uniform initializer”, you can easily find many good articles about it.

But, having read your examples, let me ask you if you remember the differences in C++ between declaration, definition and initialization - otherwise, you’d better brush up about them before going on.

Please note that syntax has been called “uniform initializer” for a good reason: it’s used to initialize things. For example, it has (normally) nothing to do with function paramethers.

Let’s look at your examples:
1
2
// Assignment
int a = 5;

That’s not usually called assignment, but initialization. This is usually called assignment:
1
2
int a = 5;    // 'a' is declared, defined and initialized (to '5')
a = 4;  // 'a' is assigned '4' 

(Well, yes, maybe people, me the first, in ‘normal’ speaking/writing don’t pay all that attention to such details, so those terms are often mixed up)

In such a case you can use {}, which is usually safer:
1
2
// becomes
int a {5};  // correct 


1
2
3
4
// Equation
b = cos(j) / 12.0 * f(y, z);
// becomes
b {cos({j}) / 12.0 * f({y}, {z})};

What is 'b'? Is it an int? We can’t know, we can only guess, because it has already been declared before. What are you initializing here, then? Here you are assigning. So, no, you need to use ‘=’.

1
2
3
4
// New Class
MyClass class1(1, 'a', 3.0);
// becomes
MyClass class1({1}, {'a'}, {3.0}}

Apart from the syntax error, these two codes make exactly the same thing, since the braces around any single element don’t change anything.

Anyway, maybe you want to re-read about std::initializer_list and member initializer list:
https://en.cppreference.com/w/cpp/utility/initializer_list
https://en.cppreference.com/w/cpp/language/initializer_list
closed account (Ezyq4iN6)
@Enoizat

Thank you for your swift reply.

It makes more sense to know that they are mainly used for initializing only. I mixed up that word with assignment. So if I understand:

1
2
int a {5}; // Proper form for initialization.
a = 5;     // Proper form for assignment. 


In my equation example, I didn't give a type for b, because it could be any type that takes the correct answer, perhaps double. I was using the example more to ask about using the {} in the function parameter passing. It seems that using {} allowed the function to produce an error when say I pass an int as a parameter that requires an unsigned int. I didn't know if it was good practice to use the {} because of that. Example:

1
2
3
4
5
6
7
8
void f(unsigned int b)
{
}

int a = -5;

f(a);     // Runs normally, but misses the type change.
f({a});   // Warns me that the types are different. 


I will take a look at those other two links you sent. Thank you for all of your help so far.
Last edited on
Topic archived. No new replies allowed.