官术网_书友最值得收藏!

Dynamic memory allocation and exception safety

Imagine that you have to write a program to rotate images. Your program takes the name of the file and the angle of rotation as input, reads the contents of the file, performs the processing, and returns the output. Here is some sample code.

 1 #include <istream>
 2 #include <fstream>
 3 typedef unsigned char byte;
 4 
 5 byte *rotateImage(std::string imgFile, double angle, 
 6                   size_t& sz) {
 7   // open the file for reading
 8   std::ifstream imgStrm(imgFile.c_str(), std::ios::binary);
 9 
10   if (imgStrm) {
11     // determine file size
12     imgStrm.seekg(0, std::ios::end);
13     sz = imgStrm.tellg();
14     imsStrm.seekg(0);        // seek back to start of stream
15
16     byte *img = new byte[sz]; // allocate buffer and read
17     // read the image contents
18     imgStrm.read(reinterpret_cast<char*>(img), sz);
19     // process it
20     byte *rotated = img_rotate(img, sz, angle);
21     // deallocate buffer
22     delete [] img;
23 
24     return rotated;
25   }
26 
27   sz = 0;
28   return 0;
29 }

The actual work of rotating the image is done by an imaginary C++ API called img_rotate (line 20). The img_rotate function takes three parameters: the contents of the image as an array of bytes, the size of the array in a non-const reference, and the angle of rotation. It returns the contents of the rotated image as a dynamically-allocated byte array. The size of this array is returned via the reference passed as the third parameter. This is an imperfect code, more reminiscent of C. Code like this is surprisingly common "in the wild" and that's why it is important to know its pitfalls. So, let us dissect the problem.

In order to read the contents of the image file, we first determine the size of the file (lines 12-13), and then allocate a byte array img just big enough to hold the entire data in the file (line 16). We read the image contents (line 18), and after performing rotation of the image through a call to img_rotate, we delete the buffer img containing the original image (line 22). Finally, we return the byte array with the rotated image (line 24). For simplicity, we do not check for read errors (line 18).

There are two glaring issues in the preceding code. If the rotation of the image failed (line 19) and img_rotate threw an exception, then the function rotateImage would return without deallocating the byte buffer img, which would thus be leaked. This is a definitive example of code that is not well-behaved in the face of exceptions, that is, it is not exception-safe. Moreover, even if everything went right, the function would return the rotated buffer (line 24), which itself was dynamically-allocated. So we leave its deallocation entirely at the caller's mercy with no guarantees whatsoever. We ought to do better.

There is a third less obvious problem. The img_rotate function ought to have documented how it allocates memory for us to know how to free it—by calling the array delete (delete []) operator (line 22). But what if there was a more efficient custom memory management scheme that the developers of img_rotate found and wanted to use in the next version? They would avoid doing so; otherwise all of their client code would break as the delete [] operator may no longer be the correct way to deallocate that memory. Ideally, this is one detail that the clients of the img_rotate API should never have had to bother about.

Exception safety and RAII

In the previous example, we looked informally at the concept of exception safety. We saw that a potential exception thrown from the img_rotate API could leak resources in the rotateImage function. It turns out that you can reason about the behavior of your code in the face of exceptions in terms of a set of criteria called The Abrahams Exception Safety Guarantees. They are named after Dave Abrahams, the Boost cofounder and an eminent C++ Standards Committee member, who formalized these guarantees in 1996. They have since been refined further by others, including notably Herb Sutter, and are listed below:

  • Basic guarantee: An operation terminated midway preserves invariants and does not leak resources
  • Strong guarantee: An operation terminated midway will not have any effect, that is, the operation is atomic
  • No-throw guarantee: An operation that cannot fail

An operation that does not satisfy any of these criteria is said to be "not exception-safe" or more colloquially, exception-unsafe. The appropriate level of exception safety for an operation is the programmer's prerogative but exception-unsafe code is rarely acceptable.

The most fundamental and effective C++ technique for making code exception-safe goes by the curious name Resource Acquisition is Initialization (RAII). The RAII idiom proposes the following model for encapsulating resources that require manual management:

  1. Encapsulate resource acquisition in the constructor of a wrapper object.
  2. Encapsulate resource release in the destructor of the wrapper object.
  3. Additionally, define consistent copy and move semantics for the wrapper object or disable them.

If the wrapper object is created on the stack, its destructor is called for normal scope exit as well as exit due to exceptions. Otherwise, the wrapper object itself should be managed by the RAII idiom. Loosely speaking, you either create your objects on the stack or manage them using RAII. At this point, some examples are in order, and we can go straight back to the image rotation example and fix it using RAII:

 1 struct ScopeGuard
 2 {
 3   ScopeGuard(byte *buffer) : data_(buffer) {}
 4   ~ScopeGuard() { delete [] data_; }
 5
 6 byte *get() { return data_; }
 7 private:
 8   byte *data_;
 9 };
10 
11 byte *rotateImage(std::string imgFile, double angle, size_t& sz)
12 {
13   // open the file for reading
14   std::ifstream imgStrm(imgFile.c_str(), std::ios::binary);
15 
16   if (imgStrm) {
17     // determine file size
18     imgStrm.seekg(0, std::ios::end);
19     sz = imgStrm.tellg();
20     imgStrm.seekg(0);
21
22     // allocate buffer and read
23     ScopeGuard img(new byte[sz]);
24     // read the image contents
25     imgStrm.read(reinterpret_cast<char*>(img.get()), sz);
26     // process it
27     return img_rotate(img.get(), sz, angle);
28   } // ScopeGuard destructor
29 
30   sz = 0;
31   return 0;
32 }

The preceding code is a modest attempt that makes the rotateImage function exception-safe, provided the img_rotate function itself is exception-safe. First up we define a struct called ScopeGuard (lines 1-9) for encapsulating character arrays allocated by the array new operator. It takes a pointer to an allocated array as its constructor argument and sets the data member data_ to this pointer (line 3). Its destructor deallocates the array pointed to by its data_ member using the array delete operator (line 4). The get member function (line 6) provides a way to get the underlying pointer from a ScopeGuard object.

Inside the rotateImage function, we instantiate a ScopeGuard object called img, wrapping the byte array allocated using array new operator (line 23). We call read on the open file stream and pass to it the raw byte array obtained by calling the get method on img (line 25). We assume read always succeeds but, in production code, we should always have proper error checks in place. Finally, we call the img_rotate API and return the rotated image it returns (line 27). As we exit the scope, the ScopeGuard destructor is called and automatically deallocates the encapsulated byte array (line 28). Even if img_rotate threw an exception, the ScopeGuard destructor would still be called as part of stack unwinding. Through the use of RAII via the ScopeGuard class, we are able to claim that the rotateImage function can never leak the buffer containing the image data.

On the other hand, the buffer containing the rotated image returned by rotateImage could be leaked, unless the caller takes care to assign it to a pointer and then duly release it in an exception-safe way. The ScopeGuard class in its current form is no good there. It turns out that Boost ships different kinds of smart pointer templates to address various use cases like these, and it is worthwhile to understand these smart pointers and the patterns of resource acquisition, and the exception safety problems they help solve.

主站蜘蛛池模板: 乌鲁木齐市| 子长县| 金湖县| 涿鹿县| 新干县| 元阳县| 密云县| 温州市| 桦甸市| 胶南市| 新化县| 胶南市| 洮南市| 凤台县| 莒南县| 丰都县| 宣恩县| 武陟县| 天台县| 铁岭县| 色达县| 佛山市| 深泽县| 儋州市| 象山县| 正蓝旗| 高邑县| 灵川县| 奉贤区| 西安市| 分宜县| 大英县| 通道| 湖南省| 西乌珠穆沁旗| 合山市| 保定市| 河南省| 清涧县| 马鞍山市| 蒲城县|