1

As a side project I'm writing a couple of classes to do matrix operations and operations on linear systems. The class LinearSystem holds pointers to Matrix class objects in a std::map. The Matrix class itself holds the 2d array ("matrix") in a double float pointer. To I wrote 2 overloaded [ ] operators, one to return a pointer to a matrix object directly from the LinearSystem object, and another to return a row (float *) from a matrix object.

Both of these operators work perfectly on their own. I'm able to use LinearSystem["keyString"] to get a matrix object pointer from the map. And I'm able to use Matrix[row] to get a row (float *) and Matrix[row][col] to get a specific float element from a matrix objects' 2d array.

The trouble comes when I put them together. My limited understanding (rising senior CompSci major) tells me that I should have no problem using LinearSystem["keyString"][row][col] to get an element from a specific array within the Linear system object. The return types should look like LinearSystem->Matrix->float *->float. But for some reason it only works when I place an extra [0] after the overloaded key operator so the call looks like this: LinearSystem["keyString"][0][row][col]. And it HAS to be 0, anything else and I segfault.

Another interesting thing to note is that CLion sees ["keyString"] as overloaded and [row] as overloaded, but not [0], as if its calling the standard index operator, but on what is the question that has me puzzled. LinearSystem["keyString"] is for sure returning Matrix * which only has an overloaded [ ] operator. See the attached screenshot.

screenshot

Here's the code, let me know if more is needed.

LinearSystem [ ] and map declaration:

Matrix *myNameSpace::LinearSystem::operator[](const std::string &name) {
        return matrices[name];
}

std::map<std::string, Matrix *> matrices;

Matrix [ ] and array declaration:

inline float *myNameSpace::Matrix::operator[](const int row) {
        return elements[row];
}
float **elements;

Note, the above function is inline'd because I'm challenging myself to make the code as fast as possible and even with compiler optimizations, the overloaded [ ] was 15% to 30% slower than using Matrix.elements[row].

Please let me know if any more info is needed, this is my first post so it I'm sure its not perfect.

Thank you!

1 Answers1

0

You're writing C in C++. You need to not do that. It adds complexity. Those stars you keep putting after your types. Those are raw pointers, and they should almost always be avoided. linearSystem["foo"] is a Matrix*, i.e. a pointer to Matrix. It is not a Matrix. And pointers index as arrays, so linearSystem["foo"][0] gets the first element of the Matrix*, treating it as an array. Since there's actually only one element, this works out and seems to do what you want. If that sounds confusing, that's because it is.

Make sure you understand ownership semantics. Raw pointers are frowned upon because they don't convey any information. If you want a value to be owned by the data structure, it should be a T directly (or a std::unique_ptr<T> if you need virtual inheritance), not a T*. If you're taking a function argument or returning a value that you don't want to pass ownership of, you should take/return a const T&, or T& if you need to modify the referent.

I'm not sure exactly what your data looks like, but a reasonable representation of a Matrix class (if you're going for speed) is as an instance variable of type std::vector<double>. std::vector is a managed array of double. You can preallocate the array to whatever size you want and change it whenever you want. It also plays nice with Rule of Three/Five.

I'm not sure what LinearSystem is meant to represent, but I'm going to assume the matrices are meant to be owned by the linear system (i.e. when the system is freed, the matrices should have their memory freed as well), so I'm looking at something like

std::map<std::string, Matrix> matrices;

Again, you can wrap Matrix in a std::unique_ptr if you plan to inherit from it and do dynamic dispatch. Though I might question your design choices if your Matrix class is intended to be subclassed.

There's no reason for Matrix::operator[] to return a raw pointer to float (in fact, "raw pointer to float" is a pretty pointless type to begin with). I might suggest having two overloads.

float myNameSpace::Matrix::operator[](int row) const;
float& myNameSpace::Matrix::operator[](int row);

Likewise, LinearSystem::operator[] can have two overloads: one constant and one mutable.

const Matrix& myNameSpace::LinearSystem::operator[](const std::string& name) const;
Matrix& myNameSpace::LinearSystem::operator[](const std::string& name);

References (T& as opposed to T*) are smart and will effectively dereference when needed, so you can call Matrix::operator[] on a Matrix&, whereas you can't call that on a Matrix* without acknowledging the layer of indirection.

There's a lot of bad C++ advice out there. If the book / video / teacher you're learning from is telling you to allocate float** everywhere, then it's a bad book / video / teacher. C++-managed data is going to be far less error-prone and will perform comparably to raw pointers (the C++ compiler is smarter than either you or I when it comes to optimization, so let it do its thing).

If you do find yourself really feeling the need to go low-level and allocate raw pointers everywhere, then switch to C. C is a language designed for low-level pointer manipulation. C++ is a higher-level managed-memory language; it just so happens that, for historical reasons, C++ has several hundred footguns in the form of C-style allocations placed sporadically throughout the standard.

In summary: Modern C++ almost never uses T*, new, or delete. Start getting comfortable with smart pointers (std::unique_ptr and std::shared_ptr) and references. You'll thank yourself later.

Silvio Mayolo
  • 62,821
  • 6
  • 74
  • 116
  • Thank you so much for that insight! All i was ever taught was raw pointers (probably to prepare me for C in later courses). The reason I went with a pointer to the Matrix objects was to avoid the copy operation when adding them to the map. `matricies["name"] = existingMatrix` required calling the overloaded assignment operator. `matricies["name'] = &existingMatrix` wouldn't work. Is there a way to put a statically allocated structure into a std::map via reference so that the underlying data does not need to be copied? – interloper.__init__ Jul 01 '22 at 23:51
  • 1
    It rarely makes sense to unique_ptrs in a map or vector -- you're usually better off putting the type directly. So `std::map matrices;` is better. The only reason I can think of for using a unique_ptr here is when the type is a base type and you want to be storing pointers to derived types. – Chris Dodd Jul 02 '22 at 00:58
  • ...and for moving an existing matrix into such a data structure, you want `matrices["name"] = std::move(existingMatrix);` which will render `existingMatrix` invalid and reuse any storage it has (including storage of any embedded std::vector) – Chris Dodd Jul 02 '22 at 01:01
  • @ChrisDodd Good point, I was getting a little too caught up in the pointer thing. Storing directly is always a good plan when possible. – Silvio Mayolo Jul 02 '22 at 03:04
  • @ChrisDodd When i use `matrices["name"] = std::move(existingMatrix);` it calls my overloaded assignment operator which takes the object as const so no move will be performed. I was thinking of keep my assignment operator as a method for performing a deep copy. Is there a way to move the matrix object without invoking the overloaded assignment operator? or should I change my assignment operator to use a move and nuke existing object? – interloper.__init__ Jul 06 '22 at 23:42
  • You should implement an overloaded move assignment operator (`Matrix::operator=(Matrix &&);`) that moves the value in addition to your copy assignment operator that does a (deep) copy. Follow the [Rule of Five](https://cpppatterns.com/patterns/rule-of-five.html) – Chris Dodd Jul 07 '22 at 01:22
  • Or better yet, don't have any ctors or assignments -- just use the default ones. That's the other big advantage of using std containers and avoiding pointers rather than rolling your own. – Chris Dodd Jul 07 '22 at 01:29
  • Oh interesting, I'm going to have to look into double references. I do want overloaded operators just to idiot proof my code (though I'll be the only idiot that'll ever see it). And my work around for the invocation of the overloaded = op in the mean time before your responded was `matrices.insert(std::pair(name, std::move(matrix)));`. Not elegant but it seems to get the job done. – interloper.__init__ Jul 07 '22 at 01:51
  • `Matrix&&` is not a double reference, even though it sort of looks like one. It's an *rvalue reference*, whereas the "usual" kind of references you use like `Matrix&` are called *lvalue references* (often shortened to just "reference" since it's the more common type). But they're two distinct things. The difference is too complicated for me to explain in the length of a post comment, but you can find plenty of resources on StackOverflow or cppreference about the concept. – Silvio Mayolo Jul 07 '22 at 03:06