3

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?

1
  • 3
    Ideally you wouldn't call resize, you'd initialize the vector (and all other members) in the constructor initiliazer list. That said, in my opinion, teaching your students how to write a vector themselves (with allocations, reallocations, cleanup, exception safety etc.) is good, as long as you also teach them how to use the existing facilities in the library.
    – Mat
    Dec 26 '20 at 8:12
7

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.

5
  • 2
    "may be implemented in a "lazy way", so they do not allocate memory immediately when they are called." - A tangent, but they probably may not - because otherwise a read-only accessor would have to allocate, and thus throw bad_alloc if that fails, but it's not allowed to by the spec. Dec 26 '20 at 10:51
  • 1
    @SebastianRedl: I think to be more precise, I have to give up some brevity. But see my edit.
    – Doc Brown
    Dec 26 '20 at 13:14
  • 2
    Thanks for this answer, and appreciate the expansion. Embarrassingly, one thing I'd forgotten about was the use of the constructor initialization list -- the textbook we use at my school (not chosen by me) actually omits that entirely. (In addition, our book & compiler are both still C++98 based, so things like constructor delegation are not available; not the same thing but a related blind spot.) Dec 27 '20 at 0:19
  • 1
    @DanielR.Collins then I'd suggest trying to change that. GCC and Clang both have great C++17 support and are free and open source. There's no reasonable excuse for using C++98 nowadays. I'd argue it's even worse in a pedagogical environment. Dec 31 '20 at 9:25
  • @BernardoSulzbach: A few days ago I proposed updating our IDE to a new version packaged with TDM-GCC v.9, which at least defaults to the C++11 standard. However, the textbook still doesn't go into details like constructor initialization lists. Dec 31 '20 at 18:08
1

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.

4
  • 1
    Thanks for this answer. I'll point out re: the "long and difficult learning curve... [that is] still necessary" (if I'm understanding you right), Stroustrup in that same lecture argues against that, and criticizes "a lot of professors [who] insist on teaching you the bad ways of doing things first". youtu.be/86xWVb4XIyE?t=4950 Dec 26 '20 at 20:15
  • "To appreciate Bjarne Stroustrup's approach for C++" I don't know why one would need to "appreciate" it. You need to merely use it; to do what you're told. You create an array of T by using a vector<T>; that's what you tell new users to do instead of new T[] and the like. There is no cleanup. And you need not introduce move operations because vector is moveable and thus so too will be the types they use with it. You're looking at modern C++ through the lens of C-with-classes and are focused on the differences instead of what modern C++ looks like to new users. Dec 27 '20 at 4:59
  • You don't need to know the exact mechanisms of how the motor on a power drill works in order to drill a hole in some wood. The same goes for using C++'s abstractions; you can use them safely without knowing about details. Dec 27 '20 at 5:00
  • That said, part of the reason that we use C++ in the curriculum is for an opportunity to discuss low-level issues from a pedagogical standpoint, so it's unlikely we'll go full-on Stroustrup doctrine in the foreseeable future. If we weren't using C++ to teach that, then it's more likely we'd not teach C++ in the first place. Dec 27 '20 at 16:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.