2

How do you declare and create a 2-dimensional array whose dimensions are constant and known at compile time, but are specified by literal const arguments to the constructor of the class that owns it?

For example...

Foo.h:

class Foo {
public:
    Foo(int rows, int cols);
private:
    int totalRows;
    int totalCols;
    char buf[4][20]; // I don't actually WANT to hardcode 4 and 20 here!
};

Foo.cpp:

Foo::Foo(const int rows, const int cols) : totalRows(rows), totalCols(cols), buf(new char[rows][cols]) {}

Main.cpp:

Foo myFoo(4,20);

I know buf(char[rows][cols]) is totally wrong... it's just there to illustrate what I'm trying to achieve.

I'm pretty sure I somehow have to use constructor-initialization syntax (like I'm using to set the values of totalRows and totalCols) ... but, for arrays, I'm not sure what that syntax actually is. I'm not sure whether such syntax even exists for array-declaration, since the use case of "constructor with const int args that can only be invoked with literals to guarantee the values are known at compile-time and thus suitable for an array declaration" is admittedly kind of an extreme edge case.

In Java, I'd just declare a char[] named buf whose value is implicitly unassigned at declaration-time, then create (and assign) it in the body of the constructor ... but, as far as I know, C++ doesn't allow that, either.

I know I could probably sidestep the issue by making buf a char* and creating it in the constructor via malloc(rows * cols) ... but that seems kind of barbaric and just seems "bad" for some reason I can't quite put my finger on.


Update

OK, it looks like @Adrian Mole's idea of using templates is the right one, but now I have a new problem... because Foo is now a Foo<4,20>, the compiler won't allow me to specify Foo or Foo* as a method or constructor parameter type... it wants them all to be explicitly specified, too.

The above notwithstanding, I just discovered that Foo.cpp now has an even bigger problem. When I try implementing Foo's other methods (which I suppose I should have mentioned), the compiler is rejecting them because "Foo is not a class, namespace, or declaration". For example, the following line now gets rejected:

int Foo::getTotalRows() {
    return totalRows;
}

... presumably, because there's no longer a class Foo. Except, it looks like I can't even temporarily hack around that by inserting <4,20> (eg, int Foo<4,20>::getTotalRows()).

new Main.cpp:

Foo<4,20> myFoo;
Bar firstBar = new Bar(&myFoo, 2,17,4);

Bar.h:

class Bar {
public:
    Bar(Foo<4,20>* srcFoo, int row, int col, int len);
    // ... snip ...
private:
    char* chars;
    int length;
}

Bar.cpp:

{
    Bar::Bar(Foo<4,20>* srcFoo, int row, int startCol, int length) 
        : chars(*srcFoo->getBuf(row, startcol, length), length(length) {}
    // ... snip ...
}

... which kind of defeats the point, because it means I've gone from having to hardcode the 4 and 20 in one place, to having to hardcode it in every single place I subsequently make use of a Foo object.

Unless... there's a way to indicate to the compiler that a method/constructor should match any variant of a templated class, and have the compiler simply auto-generate the specific flavor at compile-time. For example, something like...

Bar(Foo<>* pFoo, int row, int col, int len);

... so that if Main.cpp does something like:

Foo<4,20> myFoo;
Bar first(&myFoo,1,17,3);

... the compiler would say, "OK, I know myFoo is a Foo<4,20>, and he's calling Bar's constructor. Bar has constructor that takes any flavor of Foo<>, so I'll just pretend he declared it in the source as:

Bar(Foo<4,20>* pFoo, int row, int col, int len);

... and continue normally. Then, further down, if I did:

Foo<2,16> smallerFoo;
Bar second(&smallerFoo, 0,3,11);

... the compiler would say, "OK, smallerFoo is a Foo<2,16>. Bar's Foo*-taking constructor matches, but since we haven't used a Foo<2,16> yet, I'll have to autogenerate another set of methods and pretend the source really said something like:

Bar(Foo<4,20>* pFoo, int row, int col, int len);
Bar(Foo<2,16>* pFoo, int row, int col, int len);

In other words, have the compiler recursively apply the templating to auto-generate matching methods as well.

Does something like this exist, or do I have to go back to the drawing board unless I want to explicitly specify the <rows,cols> in every subsequent use of a Foo object (including method parameters)?

5
  • 3
    Function arguments are not constexpr so rows and cols can't be used to specify the size of the array. It doesn't matter whether they're const parameter or not. Use std::vector if possible. Commented Jul 24, 2022 at 5:22
  • 2
    You could make Foo a class template, when the line in main would look something like Foo<4, 20> myFoo;. Is that what you're after? Commented Jul 24, 2022 at 5:25
  • Additionally, buf is a 2D array, so why are you using new in the mem-initializer list? Commented Jul 24, 2022 at 5:25
  • "I'm pretty sure I somehow have to use constructor-initialization syntax" - there is no such syntax for runtime-dimensioning, because runtime-dimensioning of native arrays (variable-length arrays, or VLAs) aren't a standard C++ feature. You either have to use dynamic containers (such as std::vector or std::string), or roll your own (which would be a fools errand since they already exist). The one thing you will lose when using containers of containers is continuity of data, so if that is some unstated requirement, you'll be forced to invent. Commented Jul 24, 2022 at 5:39
  • 1
    Must Foo(10, 10) and Foo(4, 666) have the same type? If yes then you need new or std::vector. If not then template and std::array are the way to go. Commented Jul 24, 2022 at 5:59

2 Answers 2

6

You could make Foo a class template, then a separate class will be created by the compiler for each different combination of rows and cols that are used. This would avoid the need for any initialization in the constructor (for the simple class that your have outlined), as all data members could be default initialized to their desired values.

The visible difference in your main would be, rather than declaring and initializing with syntax like Foo myFoo(4, 20);, you would use Foo<4, 20> myFoo … which would create myFoo as an object of the class called (something like) Foo<4,20>.

Here's a short outline, which includes some sample member functions and how to declare/define them for such a class template:

#include <iostream>

template<int rows, int cols>
class Foo {
public:
    Foo() = default;
    void Init();
    void List();
private:
    int totalRows{ rows };
    int totalCols{ cols };
    char buf[rows][cols]{};
};

template<int rows, int cols>
void Foo<rows, cols>::Init()
{
    char i = 0;
    for (int r = 0; r < rows; ++r) {
        for (int c = 0; c < cols; ++c) {
            buf[r][c] = i++;
        }
    }
}

template<int rows, int cols>
void Foo<rows, cols>::List()
{
    for (int r = 0; r < rows; ++r) {
        for (int c = 0; c < cols; ++c) {
            std::cout << static_cast<int>(buf[r][c]) << " ";
        }
        std::cout << std::endl;
    }
}

int main()
{
    Foo<4, 20> myFoo;
    myFoo.Init();
    myFoo.List();
    return 0;
}
Sign up to request clarification or add additional context in comments.

8 Comments

Worth noting to the reader where it isn't in their wheelhouse of 'obvious', since both rows and cols must be provided at compile-time as template arguments, totalRows and totalCols is redundant and thus unnecessary. You can just use rows and cols since they're mandated.
@WhozCraig Probably, yes. But they are declared as int (not const int) in OP's code, so maybe something will be done to them at a later stage?
You should use std::array<std::array<std::byte, cols>, rows> buf; though.
@GoswinvonBrederlow Sure, that's more modern C++ and would help in things like making a copy constructor much easier. But it wouldn't address the title question, which is specifically about a char[][] array. :)
Note: you forgot to actually initialize buf as char array is default-initialized. char buf[rows][cols]{}; would do and you can drop the custom default constructor.
|
1

Use a vector of vectors.

#include <cassert>
#include <vector>
#include <stdexcept>

class Foo
{
public:
    // with std::vector you can dynamically allocate
    // a 2D array of the size you want.

    Foo(std::size_t rows, std::size_t cols) :
        m_buffer(rows, std::vector<int>(cols, 0)) // initialize all rows to 0
    {
        if ((rows == 0) || (cols == 0)) throw std::invalid_argument("invalid input size");
    }

    std::size_t number_of_rows() const
    {
        return m_buffer.size();
    }

    std::size_t number_of_columns() const
    {
        return m_buffer[0].size();
    }

    int at(std::size_t row, std::size_t column)
    {
        if ((row >= number_of_rows()) || (column >= number_of_columns())) throw std::invalid_argument("invalid index");
        return m_buffer[row][column];
    }

private:
    std::vector<std::vector<int>> m_buffer;
};


int main()
{
    Foo foo{ 3,4 };
    assert(foo.number_of_rows() == 3ul);
    assert(foo.number_of_columns() == 4ul);
    assert(foo.at(1, 1) == 0);

    return 0;
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.