I was watching a lecture by Bjarne Stroustrup from 2014, on "The Essence of C++". About halfway through he rather forcefully recommends not using new, delete, or pointers for class member data, but rather delegating that work entirely to container objects (i.e., from the STL) whenever possible. For example, he shows the following slide example at 43:18:

class Matrix {vector elem;};

Now, as part of exercise for the courses in C++ that I teach, I happen to maintain my own Matrix class (which gets used in a WeightedGraph class for data storage as an adjacency matrix), and of course I use a pointer to an array of ints, a new in the constructor, and a delete in the destructor. So I considered looking at what it would take to covert this to Stroustrup-approved style, using a vector instead.

Two things occur to me that look ugly to my eye. First, and most important, I think in the constructor to my Matrix class -- at the point when I know what the fixed size of the matrix will be -- I won't be calling the vector's constructor, but rather the function elem.resize(). (Right?) That is: I "know" that this is the moment when memory allocation will occur, but I "write" as though I'm unaware of that, and there's some existing data that I'm modifying. Second, somewhat less important, the destructor in my Matrix class will be empty (I think?), and likewise for I guess every class I write going forward. So (at least pedagogically) we just start forgetting that destructors are a thing and that resources need cleaning-up.

In turn, consider my WeightedGraph class that stores its data in an adjacency Matrix. Again, I currently have a pointer to a Matrix that gets allocated in the constructor and cleaned up in the destructor. Now if I follow this same model and give it a non-pointer Matrix for member data, I need to go add a similar resize() function to the Matrix, and basically all of its current constructors (one for a square matrix, one for rectangular, etc.) become useless. And this methodology flows to whatever classes further aggregate our classes so far. Both constructors and destructors become hidden, and we're always pretending to be "resizing" objects when really we know we're allocating memory for the one and only time.

Am I correct about this advice from Stroustrup? Is there any more expressive way of using this advice to clearly reflect that we know we're doing one-off allocation in the constructors of these objects?

有帮助吗?

解决方案

Two things occur to me that look ugly to my eye.

IMHO the things you mentioned look quite less ugly to me than they were before.

in the constructor to my Matrix class -- at the point when I know what the fixed size of the matrix will be -- I won't be calling the vector's constructor, but rather the function elem.resize()

It depends on the point where you know the size of the elem vector, but for the scetched scenario, one would usually not use resize. If you can calculate the required number of elements from the constructor's parameters, one should call elem's constructor, for example:

class Matrix
{
     Matrix(int x, int y) : elem(x*y) {}  
//...

For more complex scenarios, where you need to calculate the required size inside the constructor first, or maybe later, calling elem.resize() may be necessary.

I "know" that this is the moment when memory allocation will occur,

Do you? For making this more clear, let us assume for a moment vector::resize() or vector::vector(size_t x) are implemented in a "lazy way", so they do not allocate memory immediately when they are called.

Maybe that is not really possible, since there may be parts of the C++ standard which forbid such an implementation, but my point is: it does not matter.

The idea of abstractions is always to simplify our thought model - here a vector releases us from the burden of thinking about the lower level details of things like memory allocation, or when it happens. The important thing here is that elem provides enough elements at the time when those elements are accessed the first time - how and when the required memory allocation will happen shouldn't bother most of the programmers most of the time.

but I "write" as though I'm unaware of that,

Assume you are unaware of it. Maybe you really are.

and there's some existing data that I'm modifying.

Not sure if I understand your problem here.

the destructor in my Matrix class will be empty (I think?)

You don't even have to provide a destructor. Code which does not have to be written has a pretty low risk to contain bugs.

So (at least pedagogically) we just start forgetting that destructors are a thing and that resources need cleaning-up

That's correct and an improvement for most real-world programs. By using vectors instead of self-allocated arrays as class members one does not have to write such tedious and error-prone memory clean-up code in the constructors any more. I fail to see where this improvement is "ugly", quite the opposite.

Of course, when you are going to teach C++ and the role of constructors and destructors, the fact using a vector as a member abstracts the necessity for destructors away may not be the most helpful thing for your course. But see this as a chance: you now have an example for a Matrix class where you can provide and compare two different designs, one with low-level memory allocation, and one with vectors - this looks to me like a great learning opportunity for your students.

To your "Weighted Graph":

Now if I follow this same model and give it a non-pointer Matrix for member data, I need to go add a similar resize() function to the Matrix, and basically all of its current constructors (one for a square matrix, one for rectangular, etc.) become useless

No, you can keep the existing constructor signatures, no need for an artifical resize function, at least if you don't need dynamic resizing of the weighted graph (see above what I wrote about the vector constructor).

And this methodology flows to whatever classes further aggregate our classes so far. Both constructors and destructors become hidden, and we're always pretending to be "resizing" objects when really we know we're allocating memory for the one and only time.

Only the destructors become superfluous. And no, you won't need to add a resize operation to each of your containers, as I wrote above. For containers where the required size can be determined at construction time, constructors stay the canonical way to go, no need to change this.

其他提示

Expressiveness is in the eye of the beholder.

(Many famous programmers have used the word "expressiveness" to mean different things, sometimes in contradictory ways.)

To appreciate Bjarne Stroustrup's approach for C++, one has to be armed with intimate knowledge with the lower level working of C++, such as memory allocations, the standard library, destructors, RAII, etc.

In other words, before one can start writing C++ the Stroustrup way, one has to undergo a learning stage, a long and difficult learning curve, before one can reach a certain level of productivity.

The exercise which you have undergone, and which you now teach your students to follow, are still necessary. Without that, your students will not have the confidence (or, the outcome) that the code written is indeed correct.

The Stroustrup way is focused on reducing opportunities for programming mistakes to happen. Toward this goal, a line of code that is eliminated is a programming mistake averted. In your examples, the code that is being eliminated is the cleanup code.

This approach sometimes creates issues in terms of initialization of class members. The workarounds are well known. Static functions (helper functions) can be used to perform computation that transforms a constructor's input arguments into something that is needed to initialize an instance member. Move constructors and move assignment operators can be used to "move" something (such as a vector that has already been sized and populated with data) from the output of a helper function to an instance member.

These workarounds require even more C++ knowledge, such as "r-value", and techniques for correctly writing and using a move constructor / move assignment operator.

许可以下: CC-BY-SA归因
scroll top