Being too afraid of std::shared_ptr can lead to using references or raw pointers in the wrong places. That way leads to the usual memory safety issues.
What kind of a question is that? It's relevant, if you care about performance, reliability and code simplicity.
> What is the alternative? 'new' and 'delete'?
new and delete are part of the constructor-destructor business. "delete [] array" is a linear operation, when you have non-trivial destructors. Of course you may always just stick to trivial destructors, but the casual use of new and delete puts you into a mindset that it is entirely normal and nothing to wonder about day-to-day, and then you pay the price without being conscious of it.
There are multiple solutions:
1. Modern (concurrent?) garbage collectors are one solution.
2. Another solution is using arenas. Recently, in the compiler I'm writing in Rust I'm using the bumpalo crate, which provides an arena allocator. This allocator is not going to execute destructors of the objects stored in the arenas, when they are freed. I basically write code which doesn't rely on destructors of little objects. There is the destructor of the arena itself, but it's already a completely different picture than doing microallocations, with an unpredicted number of destructors. I can list off the top of my head places where in my code a destructor will be executed (and it's always just a single destructor, not N). I group objects according to the time at which they should be freed. So there is one arena which holds things like source code of different files (I can't mmap because of functionality provided) and arrays of tokens. This is going to be freed when the frontend finishes its job, because at that point I'm not going to quote the source code or tokens to the user (all the error messages doing that would be generated earlier). String literals have a longer lifetime. They're going to be there throughout the whole compilation process, so they go to a separate arena. I'm not making separate allocations for each separate string literal. Similarly, I'm not making separate allocations for each token array in the frontend. It also has the benefit that code is simpler, because I don't need to deal with different forms of the same data, only differing in "ownership". I have no issues like expecting in one place a String, but what I have is a &str. Or in C++ e.g. expecting a std::shared_ptr, when I have a normal inline value (whether it's on stack or part of another struct). Everything is a reference annotated with the lifetime of the arena it resides in. It's both faster and conceptually simple.
Another case: at work recently I needed to write an in-process sampling profiler to profile a subset of our code. Modern C++ would mandate that I should use a std::vector<StackTrace> type to store the collected stack traces and the StackTrace type would have a bunch of constructors defined and a destructor. What I did instead is I calculated the size of the buffer that I needed based on the sampling frequency, maximum stack trace depth, size of addresses on the target platform and the time the profiler was supposed to run for. Then I just allocated the whole thing in one go with mmap(2) with appropriate flags. Then I just appended subsequent stack traces in the buffer, holding a pointer to the end, and incrementing it each time an appropriate amount, terminating each stack trace with the 0 address, so that I know where one stack trace begins and another ends. No resizing, no destructors firing up out of nowhere, no microallocations for separate little objects, just one allocation in the beginning and one deallocation in the end. Only two points of failure, instead of N. The "modern C++" way would: be slower, consist of more code (The Rule of N), be slower to compile, have an incomparably bigger number of points of failure. Oh, using std::vector::push_back in a signal handler is also a terrible idea (dynamic allocation in a signal handler? good luck).
Another issue with destructors is that they are used for more things than just deallocation. E.g. they are used for sockets... So now you don't know whether your last packets reached the other end in TCP. For files, you don't know if an I/O error occurred. And, unlike the defer mechanism in various languages, they are invisible, so looking at some code it's not immediately obvious that some code that you do not see is going to be executed at a particular time.
Allocating things in large portions and forgoing that invisible destructor business brings a lot of benefits: performance, reliability, code simplicity.
Also, a big reason why manual memory management is so tricky and why people are so afraid of it is exactly because they were taught to allocate each little object separately, each little object having its separate lifetime. When your code is sprinkled with malloc(3) and free(3) all over the place, then no wonder you lose track of things. In my compiler, although it's written in Rust, I can easily point to places where memory allocation and deallocation happen. I could replace it with manual calls, because the objects are grouped together into larger buckets. It also helps avoid fighting the borrow-checker, when your lifetimes are so simple.
UPDATE: This also translates into efficient use of a garbage collector. If you allocate and free a lot of little objects frequently, then the garbage collector is going to be spending a lot of time on-CPU. And the developers are going to say "it's not me, it's the garbage collector!", but the truth is they are the ones that made all that garbage.
> What kind of a question is that? It's relevant, if you care about performance, reliability and code simplicity.
The topic really was std::unique_ptr VS std::shared_ptr in the context of an introduction to modern C++... We are talking about standard best practices, not advanced high performance applications.
Of the rest, the overwhelming majority of what most programs do has no particular performance requirement, so whatever is most convenient is fast enough. Usually only a small kernel of the program needs any attention to performance.
In such kernels, the overwhelming majority yield to very ordinary techniques, because (e.g.) they build their structures at startup, and just use them afterward, or only allocate occasionally.
The few remaining cases demand that you be smart, and make full use of the flexibility and power of the language and environment. If the language is not up to the job, you're stuck.
Going straight to mmapping hugetlb pages is a rookie mistake. Failing to mmap storage when the problem calls for it is another.
C++ and Rust are distinguished by supporting development at all of these levels. "Modern C++" does not imply confining yourself to any of them. A mature programmer uses the tools at hand as needed to meet requirements, without wasting time and without agonizing over ideology. Rust offers no advantage over C++ in this.