Error Handling with C++ Exceptions, Part 1

2023-10-31

                          by Chuck Allison

Error Handling Alternatives

With the traditional programming languages of yore, a developer's alternatives for handling errors consisted mainly of two options:

1. Ignore them.
2. Check return codes (fastidiously).

The first alternative should find its way only into toy programs such as school assignments or magazine articles that aren't discussing exceptions. Such a non-strategy just won't do for anything you plan to actually execute.

Consider the program in Listing 1, for example, which deletes an entire directory tree using standard POSIX directory-handling functions. (For an explanation of these functions, see my column, "Code Capsules," CUJ, June 1993). What could possibly go wrong? Well, I see the following potentialities:

  • The chosen directory doesn't exist.
  • The directory is read-protected.
  • Files in the directory are delete-protected.
  • The directory itself is delete protected.

For these reasons, most C library functions return a status code which you can test to detect if an error occurred. As Listing 2 illustrates, making use of these codes requires a lot of checking throughout a deeply nested call chain. The deeper it gets, the more tired you get. Since the goal in such cases is to return to some safe state, it would be nice if there were some way to yank the thread of execution "out of the deep" and place it higher up in the call chain.

Well, there is such a mechanism, which brings us to a third error handling alternative:

3. Use non-local jumps to re-route the thread of execution.

This is what C's setjmp/longjmp mechanism is all about.

As you can see in Listing 3, setjmp uses a jump buffer, of type jmp_buf, which is a compiler-defined structure that records information sufficient to restore control to a previous point in the program. Such information might include the stack pointer, instruction pointer, etc. A jump buffer must be global, of course. (Think about it — references to the jump buffer transcend a single function.)

When setjmp first executes, it initializes the buffer and returns zero. If a longjmp call further down in the call chain refers to the same jmp_buf, then control transfers immediately back to the setjmp call, this time as if it returned the second argument from the longjmp call. It would really confuse things if you could return a zero via a longjmp call (which you would never do, I realize), so if some other foolish programmer makes the attempt, setjmp will return a 1 instead.

You may be wondering why the volatile keyword appears in this version of the program. Compilers do funny things sometimes, and it turns out that any local variables declared in the same block as the setjmp call are not guaranteed to retain their original values after a longjmp, unless you adorn their declaration with the volatile qualifier.

It would seem that all problems are solved, and I can bid you adieu. It turns out, however, that jumping out of a function into another one higher up in the call chain can be risky in C++. The main problem, as Listing 4 illustrates, is that automatic variables created in the execution thread between the setjmp and longjmp may not get properly destroyed. Fortunately, C++ provides for such alternate returns that know about destructors, via the exception handling mechanism. So, if you program in C++, you might consider this final error handling alternative:

4. Use Exceptions.

That's what the remainder of this article is about.

Stack Unwinding

The program in Listing 5 is a rewrite of Listing 4 using exceptions. You turn on exception handling in a region of code by surrounding it with a try block. You place exception handlers immediately after the try block, in a series of catch clauses. An exception handler is much like a definition for a function, named by the keyword catch.

You raise an exception with a throw expression. The statement

throw 1;

causes the compiler to search back up the chain of nested function calls for a handler that can catch an integer, so control passes to that handler in main. The run-time environment invokes destructors for all automatic objects constructed after execution entered the try block. This process of destroying automatic variables on the way to an exception handler is called stack unwinding.

As you can see, executing a throw expression is semantically similar to calling longjmp, and a handler is like a setjmp. Just as you cannot longjmp into a function that has already returned, you can only jump to a handler associated with an active try block — one for which execution control has not yet exited. After a handler executes, control passes to the first statement after all the handlers associated with the try block (the return statement, in this case).

The notable differences between exceptions and the setjmp/longjmp facility are:

1. Exception handling is a language mechanism, not a library feature. The overhead of transferring control to a handler is invisible to the programmer.

2. The compiler generates code to keep track of all automatic variables that have destructors and executes those destructors when necessary (it unwinds the stack).

3. Local variables in the function that contains the try block are safe — you don't need to declare them volatile to keep them from being corrupted as you do with setjmp.

4. Handlers are found by matching the type of the exception object that you throw. This allows you to handle categories of exceptions with a single handler, and to classify them via inheritance.

5. Exception handling is a run-time mechanism. You can't always tell which handler will catch a specific exception by examining the source code.

The last point is significant. C++ exception handling allows a clean separation between error detection and error handling. A library developer may be able to detect when an error occurs, such as an argument out of range, but the developer doesn't know what to do about it. You, the user, can't always detect an exceptional condition, but you know how your application needs to handle it. Hence, exceptions constitute a protocol for runtime error communication between components of an application.

It is also significant to realize that C++ exception handling is designed around the termination model. That is, when an exception is thrown, there is no direct way to get back to the throw point and resume execution where you left off, as you can in languages like Ada that follow the resumption model. C++ exceptions are intended for reporting rare, synchronous events.

Catching Exceptions

Since exceptions are a run-time and not a compile-time feature, standard C++ specifies the rules for matching exceptions to catch-parameters a little differently than those for finding an overloaded function to match a function call. You can define a handler for an object of type T several different ways. In the examples that follow, the variable t is optional, just as it is for ordinary functions in C++:

catch(T t)
catch(const T t)
catch(T& t)
catch(const T& t)

Such handlers can catch exception objects of type E if:

1. T and E are the same type, or
2. T is an accessible base class of E at the throw point, or
3. T and E are pointer types and there exists a standard pointer conversion from E to T at the throw point. T is an accessible base class of E if there is an inheritance path from E to T with all derivations public.

To understand the third rule, let E be a type pointing to type F, and T be a type that points to type U. Then there exists a standard pointer conversion from E to T if:

1. T is the same type as E, except it may have added any or both of the qualifiers const and volatile, or
2. T is void*, or
3. U is an unambiguous, accessible base class of F. U is an unambiguous base class of F if F's members can refer to members of U without ambiguity (this is usually only a concern with multiple inheritance).

The bottom line of all these rules is that exceptions and catch parameters must either match exactly, or the exception caught by pointer or reference must be derived from the type of the catch parameter. For example, the following exception is not caught:

#include <iostream>

void f();

main()
{ try
{
f();
}
catch(long)
{
cerr << "caught a long" << endl;
}
}

void f()
{
throw 1; // not a long!
}

When the system can't find a handler for an exception, it calls the standard library function terminate, which by default aborts the program. You can substitute your own termination function by passing a pointer to it as a parameter to the set_terminate library function. (See Listing 6)

The following exception is caught, since there is a handler for an accessible base class:

#include <iostream>
using namespace std;

class B {};
class D : public B {};

void f();

main()
{
try
{
f();
}
catch(const B&)
{
cerr << "caught a B" << endl;
}
}

void f()
{
throw D();
}

An exception caught by a pointer can also be caught by a void* handler.

Question: Since the context of a throw statement is lost when control transfers to a handler, how can the exception object still be available in the handler?

Answer: Good question! Here is another area where exception handling differs from function calls. The runtime mechanism creates a temporary copy of the thrown object for use by the handler. This suggests that it is never really a good idea to define catch parameters with value semantics, since a second copy will be made. Also, it can be dangerous to define a catch parameter as a pointer, because you may not know if it came from the heap or not (in which case you must delete it). The safest strategy is always to catch by reference. And even though the exception is a temporary, you can catch it as a non-const reference if you want (yet another departure from the normal function-processing rules to accommodate exception handling).

Standard Exceptions

The Standard C++ library throws exception objects of types found in the following class hierarchy:

exception
logic_error
domain_error
invalid_argument
length_error
out_of_range
runtime_error
range_error
overflow_error
underflow_error
bad_alloc
bad_cast
bad_exception
bad_typeid

A logic error indicates an inconsistency in the internal logic of a program, or a violation of pre-conditions on the part of client software. For example, the substr member function of the standard string class throws an out_of_range exception if you ask for a substring beginning past the end of the string. Run-time errors are those that you cannot easily predict in advance, and are usually due to forces external to a program. A range error, for instance, violates a post-condition of a function, such as arithmetic overflow from processing legal arguments.

A bad_alloc exception occurs when heap memory is exhausted. (See "Memory Management" in Part 2 next month.) C++ will generate a bad_cast exception when a dynamic_cast to a reference type fails. If you rethrow an exception from within an unexpected handler, it gets converted into a bad_exception. (See "Exception Specifications" in Part 2.) If you attempt to apply the typeid operator to a null expression, you get a bad_typeid exception.

The program in Listing 7 derives the dir_error exception class from runtime_error, since the associated errors are detected by the return status of system services. The exception base class has a member function called what that returns the string argument that created the exception. I use this member function to pass information about the error to the exception handler.

The handler with the ellipsis specification (catch (...)) will catch any exception, so the order of handlers in program text is significant. You should always order catch clauses according to their respective types, from the most specific to the most general.

We're Not Done Yet

I know what you're thinking. Things can't be this easy. There has to be a "catch" somewhere (ha ha). Well there is. The error handling plot really thickens when allocating resources fails. Next month I'll explore that issue, and also share Chuck's Pretty Good Error Handling Strategy for C++ development. o

Acknowledgment

This article is based on material from the author's forthcoming book, C and C++ Code Capsules: A Guide for Practitioners, Prentice-Hall, 1998.

Chuck Allison is Consulting Editor and a former columnist with CUJ. He is the owner of Fresh Sources, a company specializing in object-oriented software development, training, and mentoring. He has been a contributing member of J16, the C++ Standards Committee, since 1991, and is the author of C and C++ Code Capsules: A Guide for Practitioners, Prentice-Hall, 1998. You can email Chuck at cda@freshsouces.com.

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Error Handling with C++ Exceptions, Part 1 的相关文章

随机推荐

  • [PyTroch系列-2]:Facebook PyTroch简介、生态环境、开发架构、软件架构

    作者主页 文火冰糖的硅基工坊 文火冰糖 王文兵 的博客 文火冰糖的硅基工坊 CSDN博客 本文网址 PyTroch系列 2 Facebook PyTroch简介 生态环境 开发架构 软件架构 文火冰糖 王文兵 的博客 CSDN博客 第1章
  • git远程仓库新建了分支,但是vscode分支管理里还没有这个分支怎么办

    git远程仓库新建了分支 但是vscode分支管理里还没有这个分支怎么办 git终端输入以下命令 成功后再次查看 就有了 git fetch origin 分支名
  • Jenkins基础篇--Docker容器部署

    容器部署Jenkins的优势 1 安全 容器之间的进程是相互隔离的 单独容器环境稳定 宿主机中环境变量的修改 不容易影响容器的运行结果 2 更轻松地部署和扩展 容器可方便迁移 一次交付 多次利用 容器可将打包好的jenkins环境迁移到其他
  • spark-sql提交参数详解整理

    1 spark任务提交 当SPARK SQL开发完成后需要将其提交到大数据平台上去跑 提交的时候需要对要使用的资源参数进行设置 目的 a 让任务在正确的环境下运行 b 根据任务自身情况 设置合理参数 提高运行效率 2 设置参数说明 2 1
  • python是从abc发展_Python 简介

    Python 简介 Python 是一门解释型语言 因为无需编译和链接 你可以在程序开发中节省宝贵的时间 Python 解释器可以交互的使用 这使得试验语言的特性 编写临时程序或在自底向上的程序开发中测试方法非常容易 Python 是面向对
  • 简单易懂矩阵螺旋打印

    简单易懂矩阵螺旋打印 C语言 给定一个 m 行 n 列的矩阵 请按照顺时针螺旋的顺序输出矩阵中所有的元素 输入格式 首先在第一行输入 2 个整数 分别对应题目描述中的 mm 和 nn 1 leq m n leq 1001 m n 100 之
  • 设置入校时间字段的有效性规则为_2012年计算机二级Access第二十六套上机试题及答案详解...

    1 基本操作题 在考生文件夹下 存在一个数据库文件 samp1 mdb 和一个图像文件 photo bmp 在数据库文件中已经建立一个表对象 tStud 试按以下操作要求 完成各种操作 1 设置 ID 字段为主键 并设置 ID 字段的相应属
  • UE4 C++ 用蓝图调用C++里定义的变量、方法

    UE4 C 用蓝图调用C 里定义的变量 方法 这是一个Object的C 脚本 h UCLASS Blueprintable 可被蓝图继续 class BASICTRAINING API UMyObject public UObject GE
  • 企业微信接入自研小程序流程

    一 背景 企业微信是企业内部办公常用的即时通讯工具 可以作为企业内部工作的枢纽 例如 重要内容通知 重要应用的集成等 二 自研程序接入企业微信配置 1 登录企业微信管理后台https work weixin qq com 2 找到应用管理
  • java -jar xxx.jar中没有主清单属性

    使用Spring Initailizr创建的项目 使用mvn打包后 java jar xxx jar显示xxx jar中没有主清单属性 去掉标签即可
  • Java实现阿里云短信发送功能(保姆级!!!搞懂短信功能,这一篇就够了!)

    目录 一 准备工作 1 功能如何切入 2 为什么要用阿里云来实现 二 阿里云部分 三 代码部分 OK 分享结束 收 一 准备工作 1 功能如何切入 第一步 分析业务需求 想要实现短信通知功能那就要有短信的收发双方 而手机上的短信功能需要占用
  • FFmpeg滤镜:制作图片视频流

    iPhone相册有个 为你推荐 功能 它会挑选一些照片形成一个主题 点击后可以像视频一样播放 那么 怎样才能把多张照片转成一个视频文件呢 使用FFmpeg可以这么来做 ffmpeg f image2 framerate 0 5 i D MT
  • Qt使用QSplitter实现分割窗口

    分割窗口在应用程序中经常用到 它可以灵活分布窗口布局 经常用于类似文件资源管理器的窗口设计中 然后抱着这样的想法简单的实现了下 cpp view plain copy print main cpp include
  • Android之通过BaseAdapter自定义适配器的使用

    通过BaseAdapter创建自定义适配器 在所有的适配器中 通过BaseAdapter定义的适配器非常好用 可以自定义ListView每行布局的样式 使用非常的广泛 是开发过程中必不可少的 下面看一个效果图 接下来一起来实现聊天列表 1
  • 在 ROS 中使用 Protobuf 替代 ros msg

    转自 https segmentfault com a 1190000012734275 Background 做 ROS 相关开发的 应该都知道 ros msg 有个非常大的槽点 ros msg 扩展性较差 即如果 msg 的字段发生变化
  • JAVA 输出一个会动的爱心

    以下是 Java 代码 可以在控制台输出一个会动的爱心 public class Love public static void main String args throws InterruptedException while true
  • 看完百度文心一言的魔性作图,我头都笑掉了...

    近日看到网友们用百度文心一言来作图 看了后我都愣住了 1 AI 作画 车水马龙 2 AI 作画 驴肉火烧 3 AI 作画 唐伯虎点秋香 4 AI 作画 鱼香肉丝 5 AI 作画 胸有成竹 6 AI 作画 夫妻肺片 7 AI 作画 红烧狮子头
  • 【GUI】Python图形界面(一)

    Python图形界面 一 第一个界面 1 了解模块代码的组成 导入库 PySimpleGUI 定义布局 确定行数 创建窗口 事件循环 关闭窗口 1 导入库 import PySimpleGUI as sg 2 定义布局 确定行数 layou
  • SharePreference原理

    SharedPreferences是Android提供的数据持久化的一种手段 适合单进程 小批量的数据存储与访问 因为SharedPreferences的实现是基于单个xml文件实现的 并且 所有持久化数据都是一次性加载到内存 如果数据过大
  • Error Handling with C++ Exceptions, Part 1

    by Chuck Allison Error Handling Alternatives With the traditional programming languages of yore a developer s alternativ