5 ms·
Considering how difficult it is to handle errors in C++, I am not sure you can say it is just as safe as "any" language. Exceptions can be used, except that th
by betterunix 14y ago
Considering how difficult it is to handle errors in C++, I am not sure you can say it is just as safe as "any" language. Exceptions can be used, except that they cannot propagate out of destructors, they should not propagate out of constructors, and there is no standardized way to retry the operation that threw the exception.
"you need to find a good tutorial that doesn't go low level before you need it."
In other words, don't program in C++ at all. The default numeric type in C++ is fixed-width (int or floating point), the default string type is a primitive pointer (const char *), and you still see low-level pointer types tossed around as iterators (e.g. for the standard vector class).
- alexchamberlain 14y agoThe iterator for a vector is a class... although quite a thin wrapper around a pointer, it is still a class. The difficulty to handle errors in C++ is generally bad API design left over from the C days. Exceptions used correctly make error handling exceptionally easy.
- AnthonyMouse 14y agoYour criticisms would be well taken if they were actually true: >Exceptions can be used, except that they cannot propagate out of destructors Exceptions are perfectly well allowed to propagate out of destructors. A destructor throwing an exception is only considered ill advised, because destructors are called during stack unwinding when another exception is propagating, and throwing an exception during another exception causes program termination. And object destruction is largely incapable of truly failing anyway: There will be no state remaining to be left inconsistent because the object is going away, unless the destructor is interacting with an object not being destroyed, in which case the error is actually with the other object and information about the error can be stored with that object or in an error queue and handled in due course after the stack unwinding is complete. Moreover, what's your solution to the problem? If the destructor is doing nothing but deallocating memory (and calling other destructors that only deallocate memory) then it never needs to throw and there is no problem. If it's doing something else then it's either doing something that another language wouldn't allow because it has no destructors (in which case you can refrain from doing it in C++ as easily as you can switch to one of those languages), or the other language has an equivalent to destructors and then will have the same issue with throwing during exception propagation. Do you see some solution to having it both ways? >they should not propagate out of constructors Rubbish. Constructors don't even have return values. Throwing exceptions is the canonical way of indicating construction failure. The primary benefit to non-throwing constructors is that it allows certain performance optimizations. For example, if a constructor throws during a std::vector resize operation, the vector class will undo everything that had been done during the resize operation so as to leave the vector in a consistent state. This is easy with copy constructors: Just destroy the copies and keep the originals. But if vector used the move constructor to move the existing elements to the newly allocated internal array then the originals are no longer valid. So vector's resize will only use the move constructor instead of the copy constructor if the move constructor is declared noexcept (or the compiler can statically determine that it doesn't throw), since the alternative would violate the vector's ability to maintain consist state when a move constructor throws an exception. >there is no standardized way to retry the operation that threw the exception. How about catching the exception, addressing it, and retrying the operation in the try block? What do other languages do that you feel is superior? > The default numeric type in C++ is fixed-width (int or floating point) Why do you believe this to be a serious limitation? The range of a 64-bit type is more than sufficient for the overwhelming majority of applications and for the few remaining with specialized needs (you know who you are, cryptographers and mathematicians), arbitrary precision libraries are readily available. > the default string type is a primitive pointer (const char *) The default string type is std::string. Which can even be used with the large bulk of the C library through the helper function string::c_str() which provides a "safe" temporary const char array for passing to C library functions.
- betterunix 14y ago"Exceptions are perfectly well allowed to propagate out of destructors" ...and the default behavior is program termination. "destructors are called during stack unwinding when another exception is propagating, and throwing an exception during another exception causes program termination" That was true in C++98. In C++11, the default behavior of a destructor is to call std::terminate if an exception propagates out of the destructor. This must be explicitly overridden by the programmer in order to get C++98-style behavior, which is undefined (not sure if that is better or worse than being defined as "unconditional program termination"). This does not need to be a problem. If the stack were unwound after the catch block exits (or if it explicitly unwinds the stack), there would be no double exception faults. This is not something that needs to be inefficient, nor does it need to impact performance any worse than exceptions do right now, and it has already been implemented in other languages (e.g. Common Lisp). "what's your solution to the problem?" See above. "If the destructor is doing nothing but deallocating memory (and calling other destructors that only deallocate memory) then it never needs to throw and there is no problem" Except that destructors can free other resources, and some of those other resources might throw exceptions when they are freed. A destructor might, for example, close a file; if the file cannot be synchronized when it is closed, an exception should be thrown (but the C++ standard actually requires that such an exception be ignored; after all, the programmer's job is to manually close files, right?). "If it's doing something else then it's either doing something that another language wouldn't allow because it has no destructors (in which case you can refrain from doing it in C++ as easily as you can switch to one of those languages), or the other language has an equivalent to destructors and then will have the same issue with throwing during exception propagation. Do you see some solution to having it both ways?" See above! There should not be any program code that cannot safely throw an exception to indicate an error, and the solution is to change the order of stack unwinding and catch block execution. C++ is not the only language with a double-exception problem; Java and Python have this problem also (e.g. in Java, if you have try...finally without any catch and the finally block throws an exception). The other benefit of this approach, aside from solving the double exception problem, is that it makes exceptions more useful by making "restarts" possible: the catch block can resume execution from some point defined by the thrower of the exception, which helps a lot with encapsulation (if e.g. the error can be corrected, but only the client code can correct it; for example, writing to an external disk that was disconnected before the operation could complete). The only complication is that the catch block needs a stack of its own, and that the catch block needs to have a pointer to the stack frame it should return to if the rest of the stack should be unwound. These are not major challenges, certainly not by comparison with the other challenges C++ compiler writers need to deal with. "Rubbish. Constructors don't even have return values. Throwing exceptions is the canonical way of indicating construction failure." Oh, not having return values justifies using exceptions? Destructors don't have return values. How do destructors indicate failures? Constructor exceptions are bad because they can cause program termination when objects are constructed in the global scope. Global objects are generally bad, of course, since their construction order is undefined, but they are allowed and they are sometimes used by the standard (e.g. cin, cout). This is not a hard one to solve: give programmers a way to catch exceptions that are thrown before main is called or after main exits. "How about catching the exception, addressing it, and retrying the operation in the try block?" Except that forces the client code to understand how to retry an operation, which breaks encapsulation, makes error recovery more complicated (if the exception is thrown in the middle of writing some record, you now need a way to either roll back the write, figure out where the incomplete record begins, or figure out where the last write operation failed). "What do other languages do that you feel is superior?" Common Lisp's "conditions" system, for one, or any language that supports continuations. "The range of a 64-bit type is more than sufficient for the overwhelming majority of applications" Except that integer overflow vulnerabilities are common and problematic, and occur with 64-bit integer types. Saying that 64 bits is enough for any application is kind of like saying that 640k is enough RAM for any application. If you want to use integer arithmetic, use the integers -- which means arbitrary width, or else an exception being thrown if you try to represent an integer that is too big. "for the few remaining with specialized needs (you know who you are, cryptographers and mathematicians), arbitrary precision libraries are readily available." Having code that does not do unexpected things is not exactly a "specialized need." It is pretty easy for a programmer to forget that the sum of two positive numbers could actually be negative, or that the product of two positive numbers could be positive but less than the two factors. Most people think of integer arithmetic in terms of integers, not two's complement, even when they are programming in a language with fixed-width arithmetic -- and it takes extra mental effort to remember that you are not really dealing with integers (mental effort that could be used for other things). Sure, you can use a library -- but then you need to be explicit about wanting arbitrary precision, and you don't get any automatic optimization from your compiler (e.g. if your compiler can prove that some value will always be in a particular range, it might remove an arbitrary precision operation and replace it with a fixed-width operation without creating a risk of an overflow). "The default string type is std::string" Unless you declare a string constant, in which case you get a pointer to an array of characters. C++11 conveniently adds yet another way to get a pointer to an array of characters, "raw" string literals (at least it is appropriately named), on top of the typical double-quoted string and the C++03 wide-character string literal notation. Even with std::string, bounds checking is not guaranteed. operator[] won't check bounds. String iterators can be incremented past the bounds of the string without any exceptions being thrown. You can even try to dereference the placeholder iterator returned by string::end(), with conveniently undefined behavior (would any definition actually make sense for some application or platform?). You have at(), of course, which will do bounds checking but which breaks the [] syntax you get with C-style strings, arrays, maps, and other types (at least you have the same interface as std::vector, which might be useful). This is the sort of brittleness that characterizes C++ and that makes reliable code hard to write. Everything looks great, until you step over the poorly-indicated boundary that separates "good for C++" and "you'll regret this later."