- Learning Boost C++ Libraries
- Arindam Mukherjee
- 1352字
- 2021-07-16 20:49:03
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:
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:
- Encapsulate resource acquisition in the constructor of a wrapper object.
- Encapsulate resource release in the destructor of the wrapper object.
- 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.
- Cocos2d Cross-Platform Game Development Cookbook(Second Edition)
- 測試驅動開發:入門、實戰與進階
- 程序員面試算法寶典
- Mastering PHP Design Patterns
- Scratch 3游戲與人工智能編程完全自學教程
- Hands-On JavaScript High Performance
- Python程序設計
- Flutter跨平臺開發入門與實戰
- C語言程序設計
- OpenGL Data Visualization Cookbook
- IBM Cognos Business Intelligence 10.1 Dashboarding cookbook
- Java零基礎實戰
- MySQL入門很輕松(微課超值版)
- 自學Python:編程基礎、科學計算及數據分析(第2版)
- 寫給大家看的Midjourney設計書