Skip to main content

What makes C++ an unsafe language in 2025 if we avoid raw pointers and arrays?

Created
Active
Viewed 252 times
16 replies
4

In this podcast episode of "Tech over Tea" (starting from 01:24:08), the founder of SerenityOS and lead developer of the Ladybird browser mentions that they are currently looking to switch to another language, one that is preferably garbage collected, to avoid security issues that C/C++-based software has occasionally.

As far as I'm aware, most of these issues boil down to improper handling of C style raw pointers, owned raw pointers and pointer arithmetic. Examples include use after free, reading or writing data last the array boundaries, memory leaks, etc.

However, C++ has evolved a lot in the past decade and half. Starting from C++11, for example, you can basically avoid directly using raw pointers completely by using smart pointers. While C++ still includes the foot guns from the past to maintain backwards compatibility, a solid and safe base to build software on, as soon as the known foot guns are actively forbidden and, if really necessary, confined to a minimal subset of the code base.

So, what are the features that make modern C++ still unsafe under the assumption that our development guidelines rule out the use of "dangerous" language features like raw pointers outside of smart pointers or C style arrays? If we were restrict C++ to a strictly safe subset, e.g., no access to (owned) raw pointers, no raw arrays, etc., would this subset be still a competent and usable language?

16 replies

Sorted by:
79538173
2

Pointer arithmetic is one thing - it can be avoided, but it's all depends on the strict guidelines instead of a language enforcement. It only requires to skip the guidelines once, and we're back at the 'unsafe' situation (don't get me wrong, it is possible to write reliable code with c++, but it can be hard).

What is harder to avoid or detect is the undefined behavior. That opens up Pandora's box. 'Safe' languages also tries to avoid these UBs by enforcing different rules, or giving more flexibility with the type system.

79538346
0
Author

Yes, it's always safe to assume that some human will mess it up. Here, I would like to assume a development process with proper tooling (e.g., static analyzers) and organization (e.g., committer) to ensure that the devs are following the rules.

Can you elaborate more on the challenges in writing strictly safe code in C++, maybe give an example?

Thanks for mentioning the undefined behavior. That's something I haven't thought of and it definitely be harder to avoid/spot than raw pointers and arrays.

79610842
0
  • 217k
  • 46
  • 279
  • 435

Except pointer arithmetic was never a problem in a professional context. I don't recall writing a pointer arithmetic or array out of bounds bug since forever. According to my version control, I seem to have written one in year 2006. Oh noes, this is unacceptable, I better switch to Rust...

You are correct that poorly-defined behavior is the main safety problem in C++, one that can't really be avoided due to the complexity of the language. If you need a safe language, you will therefore have to port to C which doesn't suffer from the same feature bloat and even then you have to use a safe subset of the language, because C is quite flawed too. The main difference is that in C we know all the flaws.

79541896
1
  • 5.3k
  • 4
  • 37
  • 50

You only have to include one library or framework, and your good intentions just went out the window. Can you guarantee that all the millions of lines of code you will be relying on that other people wrote are safe? Can you live without them? I think "No" is the answer to both questions.

That is kind of the leg up that a platform such as Rust has, it's all being rewritten from scratch in Rust.

79543227
0
Author

You're right! But this will always an issue with whatever language you choose. Java, Python, C#, Rust all can reference C/C++ code that internally uses, and often enough even externalizes these error-prone language constructs. Take the Python bindings for the NVIDIA Management Library as an example. It forces you to do manual memory management in Python.

79609495
0

Depending on 3'rd party code doesn't make Rust any safer, as long as Rust itself offers unsafe features which are heavily used by library developers. No matter what language it is, safety first principle is a key, other languages just don't require that many rules in the convention to ensure it.

79610807
1
  • 217k
  • 46
  • 279
  • 435

There's a bunch of myths about why C++ (and C) are "dangerous" and those myths often revolve around pointers and memory leaks. But bugs related to pointers and memory leaks are in fact just beginner-level problems one writes while still learning the language.

These are not major concerns unless you have an organization with a lot staff throughput and repeatedly hire junior programmers for work which is more qualified than they are ready for... which is no fault of any given programming language.

Now there is indeed a ridiculous stupid flaw in the C++ language - namely that Stroustrup made new and new[] two different operators. This is a language design mistake constantly leading to memory leaks and the problem is that the programmer used delete rather than delete[] and that passed compilation silently. This should have been fixed 40 years ago, instead of making up stupid coding practices like "do not handle dynamic allocation manually".

On the other hand, C++ has gone to extreme lengths dealing with memory leaks by including smart pointer classes in the standard library, something that has been refined through several iterations of the language standard.

But in general, memory leaks never was a big problem among experienced programmers. You clean up your own mess and there's destructors for that. What's the problem.

Well... one big problem would be to introduce a performance-heavy dead weight thread representing your mom, going up and cleaning up the mess you've made, because you are too irresponsible to even understand how to do so yourself. Why would I force that thread into my program when I'm an adult quite capable of cleaning up my own mess? I don't need a garbage collector because I have no habit of leaving garbage behind. Besides that, bugs should be fixed, not swept underneath the carpet. Garbage collection is ridiculous nonsense.

Also modern compilers got dynamic analysis which can find memory leaks easily enough. Before that there were stand-alone dynamic analyzers like Valgrid. If you don't use static as well as dynamic analysis of your code, well that's a quality problem in your software workflow not really related to any given programming language. There are no perfect programming languages, you always need a quality system which includes testing, or you'll produce bugs no matter language.

I wrote a little article about the problems of dynamic allocation in embedded systems here: Why should I not use dynamic memory allocation in embedded systems? Among the many problems mentioned, some are far greater concerns even in generic or hosted system C++ programming: particularly the execution overhead and user data fragmentation problems. These are genuine real-world performance problems and in higher level languages than C++, you don't even get to solve these problems: they are built-in with the picked language.

As for safety concerns of C++, they are of an entirely different nature not the slightest related to pointers and dynamic allocation! The main problem with C++ is feature bloat, complexity and the countless forms of poorly-defined behavior that comes from it. No living person even knows of all forms of pooly-defined behavior existing in the C++ language. Now that is a legitimate reason to avoid C++ like the plague. If you are to avoid a language, make sure to do so for the right reasons.

79610849
0
  • 217k
  • 46
  • 279
  • 435

Btw it is curious that nobody in these kind of discussions mention one of the biggest safety hazards ever created: exception handling, a.k.a spaghetti programming on steroids. I don't think any other language feature in history comes close to creating as many bugs.

Goto considered harmful? Goto is a summer breeze compared to BOOM!!!nullpointerexceptionwheredidyoucomefromwheredidyougobzzzz... core dumped.

79610877
1

At least exceptions are hard to ignore, and coredumps are easy to analyze. Forgetting to check on the global errno and continue the execution when something already went wrong is way more fun!

But yes, error handling in general also a pain point in programming. Even in safe languages.

79611868
0
  • 217k
  • 46
  • 279
  • 435

Yeah they are hard to ignore for the users, who have to suffer the buggy mess you delivered, because you programmed in a language which supports exception handling. It's like building a skyscraper out of glass and signaling that there's a broken sink on level 55 by throwing a massive rock through all levels below and toppling the whole building, killing and destroying everyone and everything, completely out of proportion for the minor local error.

errno is ancient Unix crap from the 1970s so it's not really a suitable comparison. Obviously errno was one huge, skunky design mistake and that's not how you do error handling in the 1980s or later.

Proper error handling is rather done by returning a result code to the caller, so that errors/result remain local to the part of the program where they make sense, with loose coupling from totally unrelated parts of the program. The caller can then decide how to deal with the error, perhaps return an error further up into the call hierarchy - not necessarily the same type as the lower layer returned.

And so you end up with an error handler at the top layer of the program. Well-designed programs centralize all error handling there, at a single place, and they might also centralize decision making of which code to execute next at the same place.

If exceptions were only allowed to be thrown to the caller and not out in the wild, they might actually have been useful. But alas they were designed by very incompetent people who didn't know what they were doing. Why semi-modern languages decided to support exception handling is beyond me, we already knew it was a complete mess by the time Java was released in the late 90s.

79612146
0
Author

Since exceptions are some kind of unannotated jump statements, I guess exceptions are harmful in C++ in combination with manual resource management because exceptions handled by upper-level code can result in memory leaks and other unclosed resources if not considered properly. So, I wouldn't necessarily see exceptions as the problem here, but rather using malloc/free style code.

And so you end up with an error handler at the top layer of the program. Well-designed programs centralize all error handling there, at a single place, and they might also centralize decision making of which code to execute next at the same place.

Actually, you don't want to handle all errors at a central place. It really depends on the context where an error is most suitably handled. For example, when calling a function, such as find_item_in_list that throws a NotFound exception if the item is not in the list, you probably want to handle that right in the caller and not somewhere in your main (you could also return null, but what if it's null that you're searching in your list?). Conversely, when you have some unrecoverable system error, you probably want the program to stop immediately and handle this in your main to provide the user with some useful information.

Java has an explicit distinction between these two kinds of errors. Exceptions are part of the function signature and have to be handled explicitly by the calling function. Runtime errors are not part of the function signature and are propagated upwards until some code (optionally) handles them.

I find error codes in C++ cumbersome to work with. Usually, I would like the return value to be something for me to process and it clutters my code with additional branches for checking the error code every time I call the function. So, personally I prefer exceptions over error codes.

However, I've come to like the Rust way of doing error handling. It does away with all these issues by simply wrapping the result value in an enum struct that is either a Result containing a result value or an Error with error information. You can choose to process the error in-place or propagate it upwards. Furthermore, the error is always part of your function signature, so there are no surprise exceptions from deep within some library.

79612179
0
  • 217k
  • 46
  • 279
  • 435

Since exceptions are some kind of unannotated jump statements, I guess exceptions are harmful in C++ in combination with manual resource management because exceptions handled by upper-level code can result in memory leaks and other unclosed resources if not considered properly. So, I wouldn't necessarily see exceptions as the problem here, but rather using malloc/free style code.

Leaks caused by exception is one problem but far from the biggest one. The main problem is spaghetti programming execution - the program jumping from one place to another completely unrelated place - often without the programmer expecting it or with precautions in place. Exception handling is inherently broken by design and C++ is far from the only language suffering from it.

79612187
0
  • 217k
  • 46
  • 279
  • 435

Actually, you don't want to handle all errors at a central place. It really depends on the context where an error is most suitably handled.

Yeah but that wasn't really what I said - I rather said that an error should be handled locally if that is possible - or in case it isn't, passed along the result to the caller. The error could also be handled locally but reported elsewhere. And so ultimately you will need a top level error handler. This means that user notifications, error logging and similar can be handled from one single place, likely attached to the GUI.

79612206
0
  • 217k
  • 46
  • 279
  • 435

I find error codes in C++ cumbersome to work with. Usually, I would like the return value to be something for me to process and it clutters my code with additional branches for checking the error code every time I call the function. So, personally I prefer exceptions over error codes.

Error handling will always clutter down the code. It is extra functionality and so it will take space in the code like everything else. Not the must creative kind of code to write, but important none the less.

It adds no extra branches. Because a function using exception handling would still have to do if(error) throw rotten_tomatoes. If you move that branch to the caller side instead, the number of branches remain the same. The alternative: result = OK; .... result = error_code; ... return result; followed by if(result == error) in the caller code has the same number of branches. Also, once you have noted that a critical error occurred, the code is likely not performance critical from there.

Also note that catch(specific_exception) is implemented as a branch. catch(specific_exception) { } catch(...) is two branches, same with Java finally.

As a professional programmer you also have to weigh your rationales against each other. "Oh no I have to type some extra error handling code" is a weak argument for doing something. "Exceptions may crash and destroy every single thing in my program and cause the program counter to run havoc" is an incredibly strong argument for not doing using exceptions. Here application programmers have a lot of learn from mission-critical systems where you can't just shrug off bugs as if they are a natural phenomenon destined to happen. Bugs happen with bad program design and when using dangerous features implemented by misguided language designers, like exception handling.

In general, programmers who are afraid of typing extra text and uses that as an argument for their coding style should consider another career, because - surprise! - programming is all about typing text.

79612837
1

Even if you avoid raw pointers and arrays, C++ is still considered an unsafe language in 2025 due to several fundamental reasons. First, C++ does not enforce bounds checking on modern containers like std::vector, which means accessing out-of-bounds elements can lead to undefined behavior (UB) without any runtime errors unless you use functions like .at(). Additionally, C++ is riddled with UB scenarios such as signed integer overflow, use-after-free, dangling references, and misaligned memory access, which can be easily triggered by mistakes in code, often going unnoticed until runtime. Unlike languages like Rust, C++ does not guarantee memory safety, and while smart pointers like std::unique_ptr or std::shared_ptr help, their misuse—such as creating reference cycles with shared_ptr—can still lead to memory leaks. Furthermore, the C++ type system allows type punning and casting, which can subvert type safety, potentially causing hard-to-track errors. Even without raw pointers, manual resource management in C++ is prone to issues like exception handling mismanagement, misuse of move semantics, and unsafe destructors. Additionally, C++ provides threading support but leaves the responsibility of handling data races, deadlocks, and atomicity to the developer, making it easier to introduce race conditions. While modern tools like static analysis and sanitizers can catch some of these issues, C++ remains inherently unsafe compared to newer languages like Rust, which were designed with strict memory safety and concurrency rules at their core. Therefore, C++ still exposes developers to a wide range of risks unless strict discipline is maintained throughout development.

79613676
0
  • 217k
  • 46
  • 279
  • 435

Again:

  • Memory leaks is not a big problem and even when such bugs are present, there's a big chance that they are 'dormant' bugs which won't cause any erroneous behavior apart from increased RAM usage.

  • Out of bounds bugs is not a big problem since they are easily found both by manual code review and static/dynamic analysis.

These are beginner-level bugs. They are not the reason why C++ is unsafe and they are not the reason why Rust is safe (in "safe mode").