Hacker News new | past | comments | ask | show | jobs | submit login
C++ Constructors, Memory, and Lifetimes (erikmcclure.com)
47 points by blackhole on May 2, 2021 | hide | past | favorite | 38 comments



As you are officially discouraged to use new and delete directly (https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines...) and I fully agree with this guideline, I was disappointed to see new being mentioned so early and not immediately being followed up with std::unique_ptr and std::shared_ptr. Especially because you introduced the constructor/destructor subsection with how destructors can be used to free memory. A general mention of std::shared_ptr would have been expected as well, I think.

It is unfortunate that many learning resources don't show modern C++ so the language is perceived worse than it is and also more bugs are written, than is necessary.


In my opinion, the site you linked to has a major problem - many of the example functions have a "void" return type. This indicates that they must have side-effects (or why else call them?) and we would normally not want to write functions that have side effects.

A rule of thumb: if your function doesn't have a return type, consider that your design maybe wrong in some way.


What the functions return is utterly irrelevant for the examples, so adding return types and statements just makes the example more complex for no good reason (and distracts from examples where the example is about the returns)


The site is about writing "good" C++. Having loads of functions that implement "bad" C++ cannot help.


Good C++ code shouldn't throw around heap allocations, shared or otherwise. Smart pointers are like duct tape, they help keeps things together but most of the time there are better solutions. Your complaint about void will be noted once you fix your programs ownership semantics.

Note: Any negative reactions to smart pointers on my side may be fueled by a disturbing amount of cargo culting that I had to witness since their inclusion in the standard.


> Your complaint about void will be noted

By whom?

> once you fix your programs ownership semantics.

How do you heck know how my programs need fixing?

> Any negative reactions to smart pointers on my side may be fueled by a disturbing amount of cargo culting that I had to witness since their inclusion in the standard.

Any positive reactions to smart pointers on my side may be fuelled by over 30 years experience in using them.


> How do you heck know how my programs need fixing?

You where the one complaining that sample one liners with void return types where bad code. In my experience shared_ptr is overkill in nearly all trivial code examples, so any one liner containing one is bad code.

> Any positive reactions to smart pointers on my side may be fuelled by over 30 years experience in using them.

Hope you never had to deal with code reviews that suggested putting scoped std::vector uses in shared_ptr s to avoid stack overflows. Or really any code that put objects into smart pointers because it could not because it should.


I have never, ever used, or suggested the use of shared_ptr, which I believe is not a construct that should be in C++ or any other language.


> (...) shared_ptr, which I believe is not a construct that should be in C++ or any other language.

What's your rationale for that assertion?


This is true, but two points: Firstly, don't teach too much at the same time, and secondly sometimes you have to just launch the missiles.


> The pointer returned by new should belong to a resource handle (that can call delete). If the pointer returned by new is assigned to a plain/naked pointer, the object can be leaked

Unless I'm reading this wrong (and I'm seeking to be corrected here), it's still fine to use new/delete in certain contexts

  struct bla {
    int n;
    int[] mem;
    bla(int n) : n(n), mem(new int[n]{}) {}
    ~bla() { delete[] mem; }
    /* forbid copies (for uniqueness) and moves (to keep this short) */
    bla(bla &) = delete;
    bla(bla &&) = delete;
    bla &operator=(bla &&) = delete;
  };
Edit to remove unnecessary null-checking


Note above, the “delete” operator is safe to call on nullptr values, and it is also not necessary to set pointers to nullptr in destructors (since the object is going away anyway). The example above therefore could just say “delete [] mem;” in the destructor with nothing else.

Yet this is also a good example of why you wouldn’t need “delete” or a destructor at all, if “mem” changes to std::unique_ptr<int[]> type.

And even then, you should probably seriously consider std::vector or std::array before trying a unique_ptr array.


should. It's a guideline to avoid mistakes, it doesn't mean you can't do memory manually in C++, it's just recommended you avoid it unless you have a good reason.


That struct should be just struct bla { int n; std::unique_ptr<int[]> mem; bla(int n) : n(n), mem(std::make_unique<int[]>(n)) { } }. There is no reason to use new/delete there.


You could just use std::vector<int> there


Of course you could. That wasn't my question -- my question was about the wording of the recommendation; "...should belong to a resource handle (that can call delete)"


Is:

     int[] mem;
even legal C++?


Not standard C++.

If the [] was after the variable name, because it is the last member, most C++ compilers (GCC, Clang, and MSVC) have a non-standard extension: https://en.wikipedia.org/wiki/Flexible_array_member


Not even non-standard C++ - my g++ compiler errors with:

a.cpp:2:13: error: expected unqualified-id before '[' token


> If the [] was after the variable name


no, that was me being sloppy typing code on my phone


This is full of misunderstandings. When variables are allocated on the stack, the space for them with all compilers I have ever used (or written) is allocated on the stack once by adjusting the stack pointer - i.e. for all of the variables of the function. There is no pushing or popping - function variables have addresses on the stack. When the function ends, the stack pointer is reset to it's original value, possibly with destructors for the things on the stack being called.

Also, in modern compilers, parameter variables are mostly passed to functions via registers, with the stack not being involved at all.


> Also, in modern compilers, parameter variables are mostly passed to functions via registers, with the stack not being involved at all.

Given that this is a C++ thread I'll be pedantic and say that this is really the ABI rather than the compiler as per se.


ABI is a factor in every language;* so I’m not sure I understand the point you’re trying to make.

* even interpreted ones, though the calling conventions and layout are quite different from what is typically written up in the documentation of most compiled languages.


The term "modern compiler" was used, which implies that an old compiler might (say) not do so despite complying with some ABI, i.e. the registers aren't an optimization.


I have never used a compiler that didn't allocate frames in a single move of the sp, and I've been using compilers since the 1970. Surely "modern" has to mean some change more recent than that.


That isn't the part of your comment I was block-quoting.


Given that the C++ Standard has nothing to say about an ABI (neither does the C Standard, for that matter), then what a compiler does on a particular platform, and what a platform ABI specifies (if it does) are possibly kind of linked.


The word compiler is only mentioned 12 times in the whole standard give or take.

My point was that it's not a question of whether the compiler is modern or not, i.e. I work with an old compiler backend on a modern system and it still gets the ABI correct even if it ends up spilling back to memory too often.


I took it as a "conceptual abstraction". It presented a mental model focused on the relationship between variables' lifetimes, intentionally glossing over the implementation details of any particular compiler.


If it is glossing over implementation details, why does it mention the stack? That _is_ an implementation detail.

AFAIK (I never looked at an official standard, only at drafts) the standard doesn’t mention the word “stack” at all

A C++ compiler is free to allocate environment frames for every block on the heap.


There are different tiers of abstraction. This post picked a level of granularity that the author felt would be helpful for a certain subset of readers.

Whether the "stack" is formalized or not, it's the mental model (and the vocabulary) that's most often used when working with this stuff. I don't think there's anything wrong with the fact that they focused on the forest and not the trees.


Yes, there are different tiers of abstraction, but I think this article, about lifetimes, would be better if it just discussed scope.

As is, it goes into implementation details for block scoped storage, but just posits that new and delete exist without mentioning any detail. That’s unbalanced.

Rereading it, it also assumes you know C well enough to know about malloc, so maybe, just saying “C++ can automatically do stuff when variables go out of scope or when you allocate or free memory. Here’s what it can do…” might be sufficient)


Since this would be too easy for c++, there is also (sometime mandatory) copy elision.


> When variables are allocated on the stack, the space for them with all compilers I have ever used (or written) is allocated on the stack once by adjusting the stack pointer - i.e. for all of the variables of the function.

it's not a given

    int algo(int*, int n);
    int computation(int i, int n) {
        int alloc[n];
        return algo(alloc, n);    
    }
on some compilers (Keil for instance afair), alloc will be heap-allocated (and freed when leaving the function) even if it's "automatic storage" (what most people call the stack).


Isn't variable sized array non-standard in C++? Variable sized array is kind of a special thing anyway so I wouldn't be surprised if some compilers do something special with it.

Yes, the term "automatic storage" is more accurate here.


>This means it allocates memory from the heap, named after the heap data structure

Oh my god, please don't write articles about things that you barely understand!


Another one:

> the exact order that C++ evaluates expressions is extremely complicated and not always defined, ... "

"(Un)defined" and "unspecified" (which order of evaluation of fun-args really is) are two distinct terms in the C++ spec meaning different things, they cannot be used interchangeably.

But I guess it's just yet another thing of "Pedantic assembly-code analysts"...




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: