Title Simplifies Unusual Security Code ELMAR (Reposted) Keyword Abnormality Home http://www.c-view.org/journal/005/gp_aa.htm
Despite a bit self-selling, I still have to tell you at the beginning, there is a wonderful content in this article. Because I persuaded my good friend Petru Marginean and I collaborate to write this article. Petru has developed a library that is useful for processing exceptions. Let's improve its implementation, where we get a refined library, in a specific case, it can greatly simplify the writing of an abnormal security code.
In the abnormal situation, you need to write the correct code is not an easy task, let us face it. An exception established a separate control flow, which has little relationship with the main control flow of the application. To understand the abnormal control flow requires a different way of thinking and requires a new tool.
The code to write an exception is difficult: An example, for example, you are developing a timely fashionable instant messaging server program. Users can log in and log out and can send messages to each other. You have a server-side database to save user information, and log in in memory. Each user can have a list of friends, and this list is saved while in memory and database.
When a user adds or deletes a friend, you need to do two things: update the database, and update the user's cache in the memory. It is such a simple matter.
Assuming that each user's information is represented by a class called User, the user database is represented by the UserDatabase class. Add a friend's operation looks like this:
class User {// ... string GetName (); void AddFriend (User & newFriend); private: typedef vector
Surprisingly, only two rows of user :: addFriend hides a fatal error. In the case of the memory, Vector :: Push_back will indicate the operation failed by throwing an exception. In that case, you can finally add friends to the database, but not adding to memory information. Now we have encountered problems, is it? In any case, the lack of information consistency is dangerous. It is likely that many places in your application assumes that the information and memory in the database are synchronized.
A simple solution is to consider the order of exchange of two lines:
void User :: AddFriend (User & newFriend) {// Add the new friend to the vector of friends // If this throws, the friend is not added to // the vector, nor the database friends_.push_back (& newFriend); // Add the new friend to the database pdb_-> addfriend (getname (), newfriend.getname ());
This does protect the data consistency in the case of Vector :: Push_back failed. Unfortunately, when you look at UserDatabase :: AddFriend document, you find that it will also throw an exception. Now you will add your friends to the vector, but not add to the database. At this time, you will be questioned by the database: "Why don't you return an error code, but to throw an exception?" They will say: "We use a highly reliable cluster database server running on the TZN network, Extract. Therefore, we think should use an abnormality to indicate that an error is the best, because the exception only appears in an abnormal situation, isn't it? "This reason is to speak, but you still have to deal with an error. You won't want the entire system to be confusing because the database is wrong. This way you fix the database when you fix the database.
Essentially, you have to do two operations, any of them may fail. When one of them fails, you must revoke all operations. Let's take a look at how to do this.
Method 1: Rude method A simple approach is to throw an abnormality in the try-catch block.
Void User :: AddFriend (user & newfriend) {friends_.push_back (& newfriend); try {pdb_-> addfriend (GetName (), newfriend.getname ());} catch (...) {friends_.pop_back (); throw }}
If vector :: push_back failed, there is no problem because UserDatabase :: AddFriend will not be executed. If UserDatabase :: AddFriend fails, you capture this exception (no matter what type), then you call vector :: POP_BACK to revoke the PUSH_BACK operation, then throw the same type of exception again. Such a code can work, the cost is increasing the amount of code and looks bloated. The procedure of the two lines became six lines. Imagine if your code is such a try-catch statement everywhere, it is terrible, so this method is not attractive.
Moreover, this method is not well expanded. If you have three operations to do, the code written by this method will be bloated. You have two almost bad ways option: with nested TRY sentences, or additional signs that make the process more complex. This method will lead to many problems, such as code expansion, efficiency decline, and most important understandability and maintainability.
Method 2: Principle (Political), if you see the above code to any C expert, it is likely to tell you: "Ah, the method is not good. You should use the usual method RAII (Resource Acquisition IS Initialization) , Resource allocation is initialized) [1], in the event of an error, rely on the destructor to release resources. "
OK, let us go along this road. For each operation you need to revoke, you need a corresponding class. This class constructor "Do" this operation, and the destructor "revoke" this operation unless you call a "submit" function, then The destructor does not do anything.
Use some code to clear all this. For PUSH_BACK operations, we write a vectorinserter class, just like this:
Class vectorinserter {public: vectorinserter (std :: vector
void User :: AddFriend (User & newFriend) {VectorInserter ins (friends_, & newFriend); pDB _-> AddFriend (GetName (), newFriend.GetName ()); // Everything went fine, commit the vector insertion ins.Commit (); }
AddFriend now has two different parts: action phase - complete operation; submission phase - not throwing an exception, just stop all revocation work. AddFriend works very simple: if any one fails, then you can't reach the submission point, all operations will be canceled. Vectorinserter will drop the added data POP_BACK, so the program remains in the previous state before calling AddFriend.
This method is working very well in all situations. For example, when the vector insert fails, the destructive function of INS will not be called because INS has not been successfully constructed.
This method is very good, but in the real world, it is not so simple to do. You must write a small pile of small classes to support this method. Additional classes mean that there is an additional code, multi-feet, and more entries in your class broker. Moreover, you will have more places to handle abnormal security issues. In order to revoke a new class in the destructor, it is not the smartest way from the productivity.
Oh, there is a bug in VectorInserter, have you noticed? The compiler will cause the copy constructor that hikely generates causes an error: If the copied object has not been submitted, then there may be too much POP_BACK operation in the later destructor. Defining a class is very difficult, this is another reason we have to avoid writing a lot of classes.
Method 3: Reality Method In the real world, when the programmer is sitting down and writing AddFriend, or he has seen several options above, or he doesn't have time to care about it. After the day, you know what the real results are usually? Of course you know:
Void User :: AddFriend (user & newfriend) {friends_.push_back (& newfriend); PDB_-> addfriend (GetName (), newfriend.getname ());} This is a solution based on "not-TOO = Scientific" argument: translation Note: I think the author's point of view is that the programmer in reality will use the "Tai" complex technology or programming specification to improve the security and guarantee development of the program for "less". The progress of the progress is selected and the progress is considered "Science". In fact, these two are not "fish and bear's palms, not good", which is the purpose of introducing GP reusable template technology in this series of articles.
"Who said that the memory will be used up? This machine has half G memory!"
"The program will crash because there is no memory? If you want, memory exchange has long soon slowly like a snail."
"The guy who is doing a database says addFriend is almost impossible. They use XYZ and TZN!"
"This is too much trouble, it is not worth it. I will consider it again."
A method If you need a lot of constraint rules and complain, then it is not attractive. Under the pressure of progress, a good but clumsy method will become un practical. Although everyone knows, they should do it in books, but they always like to take shortcuts. The only way is to provide a reusable solution, correct and easy to use.
When you take shortcuts, because you know your code is not perfect, you will be with a unpleasant mood Check in (the translator's note: I think this is the code management Check in, in a coding phase, one module The completed sign is to put the code Check in to all code. I think the original text means that although Coding is finished, it can also be tested, complying with the standard of CHECK IN, forced progress, you must also check in code, but Because you know there is a hidden danger, there is an imperfect feeling) your code. But this kind of mood will gradually disappear because all tests can pass. But over time, those "theoretical" will cause problems, or will start from reality.
You know that you have encountered problems, and it is a big problem: you gave up the control of the correctness of the application. Now, when the server program crashes, you don't have a lot of clues to find the wrong: hardware failure? Real bug? Is it still chaotic state due to an abnormality? What you have encountered is not an unhealthy bug, but you deliberately introduced.
Even in a period of time can work, things will always change. The number of users will increase, causing memory to reach the limit. Your network administrator may ban memory paging systems in order to ensure performance. Your database may not be so reliable. You are not prepared to this.
Method 4: Petru method with scopeguard - we will introduce later - you can easily write simple, correct and efficient code:
void User :: AddFriend (User & newFriend) {friends_.push_back (& newFriend); ScopeGuard guard = MakeObjGuard (friends_, & UserCont :: pop_back); pDB _-> AddFriend (GetName (), newFriend.GetName ()); guard.Dismiss ( }
In the above code, the unique task of Guard object is to call Friends_.pop_back when it leaves the scope, unless you call DISMISS. If you call, then Guard will not do anything. ScopeGuard implements automatically calls a full-class function or member function in its destructor. In the case of abnormal, you will want to implement the function of automatically revoking atomic operations, this time ScopeGuard will be useful. You can use ScopeGuard: If you want a few actions work in a way "either do, either don't do", you can put a ScopeGuard immediately after each operation, this scopeguard can cancel the previous operation:
Friends_.push_back; scopeguard guard = makeobjguard (Friends_, & usercont :: pop_back);
ScopeGuard can also be used for normal functions: void * buffer = std :: malloc (1024); scopeguard freeit = Makeguard (std :: free, buffer); file * TOPSecret = std :: fopen ("cia.txt"); scopeguard closeit = MakeGuard (std :: fclose, topsecret);
When the entire atomic operation is successful, you dismiss all Guard objects. Otherwise, each ScopeGuard object will be loyal to the function you have passed when you construct it. With scopeguard, you can simply place various undo operations, and no longer need to write special classes to do things such as deleting the last element of the Vector, release things such as memory. This makes ScopeGuard an extremely useful, and reusable solutions to write exception security codes, which makes everything easier.
Implementing ScopeGuardScopeguard is a promotion of typical implementation of C customary RAII (resource allocation, initialization). Their difference is that scopeguard only focuses on the part of the resource cleaning - the resource allocation is done by you, and the release of ScopeGuard handles resources (in fact, it can be argued that cleanup work is the most important part of this proverb).
There are many forms of release resources, such as calling a function, calling a function, or calling an object's member function, and each way can have zero, one or more parameters.
Naturally, we model these variants through a class level relationship. The destructor of the objects in the level completes the actual work. The roots in the level are the ScopeGuardImplbase class, as follows:
class ScopeGuardImplBase {public: void Dismiss () const throw () {dismissed_ = true;} protected: ScopeGuardImplBase (): dismissed_ (false) {} ScopeGuardImplBase (const ScopeGuardImplBase & other): dismissed_ (other.dismissed_) {other.Dismiss () ;} ~ ScopeGuardImplBase () {} // nonvirtual (see below why) mutable bool dismissed_; private: // Disable assignment ScopeGuardImplBase & operator = (const ScopeGuardImplBase &);}; ScopeGuardImplBase centralized management of dismissed_ flag, the control flag is derived Does the class want to perform cleanup work. If Dismissed_ is true, the derived class does not do anything in their destructor. Now let's take a look at the missing Virtual in the ScopeGuardImplBase destructor. If the destructor is not Virtual, how can you expect the patterns to have the correct polymorphism? Ok, keep your curiosity for a while, there is a king card in our hands, we can get a polymorphic destructure behavior without having to pay the price of virtual functions.
Now let's take a look at how to implement such an object, which calls a function or functor with a parameter in the destructor. However, when you call Dismiss, then this function or functor will not be called.
template
To make it easy to use ScopeGuardImpl1, we write an auxiliary function. Template
Makeguard depends on the compiler to derive the template parameters in the template function so you don't need to specify the template parameters of ScopeGuardImpl1. In fact, you don't need to create ScopeGuardImpl1 objects. This trick is also used by a function in some standard libraries, such as Make_Pair and Bind1st. Are you curious about the way to get a polymorphic descent behavior? Below is the definition of ScopeGuard, it will make you a shocking thing is that it is just a typedef:
Typedef const scopeguardImplbase & scopeguard;
Well, let us unveil all mysteries. According to the C standard, if the reference is initialized to a reference to a temporary variable, it will make this temporary variable life. Let us give an example to explain this matter. If you write: file * topsecret = std :: fopen ("cia.txt"); scopeguard closeit = makeguard (std :: fclose, topset); then Makeguard creates a temporary variable, its type is (see you before Deep breath): ScopeguardImpl1
This is because std :: fclose is a function that accepts the File * type parameter returns INT. Temporary variables with the above type are assigned to const references Closeit. Based on the C language rules mentioned above, this temporary variable will have the same length of survival with its reference closeit - when this temporary variable is destructed, the correct destructor is called. Then, the destructor is turned off. ScopeGuardImpl1 supports a function (or functor) with parameters. It is easy to write a class without parameters, two parameters or classes with more parameters (ScopeGuardImpl0, ScopeGuardImpl2 ...). When you have these classes, you can override Makeguard to get a beautiful, unified syntax:
Template
Up to now, we have a powerful tool to express atomic operations that call a set of functions. Makeguard is an excellent tool, especially it can also be used in the C language API, without writing a lot of packaging classes. Better is, it does not lose efficiency because it does not involve virtual function calls.
Scenguard for objects and member functions, everything is very good, but how to call the member function of the object? In fact, this is not difficult. Let us implement ObjscopeguardImpl0, a class template that can call the object's non-parameter member function.
template
ObjscopeguardIMPL0 has a little special because it uses less known syntax: pointing to the pointer and operator of the member function. * (). To understand how it works, let's take a look at the implementation of Makeobjguard (we have used Makeobjguard in this section). template
Will create a type of object: ObjscopeguardImpl0
Fortunately, makeobjguard lets you write the type that is just like a word symbol. Working mechanism is still the same - as the Guard leaves the scope, the sect of the temporary object will be called. The destructor is called a member function by pointing to a member's pointer. Here we use. * Operator. Error handling If you read HERB SUTTER about unusual books [2], you will know that a basic principle: the destructive function should not throw an exception. A description of the abnormality will make you unable to write the correct code and will stop your app without any warning. In C , when an exception is thrown, a destructory function throws another exception when the stack is unwinding, the application will be terminated immediately.
ScopeGuardImplx and ObjscopeguardImplx call an unknown function or member function, and that function may throw an exception. This will terminate the program because we design Guard's destructor's designer is: When there is an abnormality, this unknown function is called when there is an unwinding stack! In theory, you should not pass the function that may throw an exception to Makeguard or makeobjguard. In practical (you can see from the code for download), the destructive function takes defense measures to exceptions.
template
Yes, Catch (...) does not do anything. This is not written, which is very basic in the field of abnormal processing: if your "revocation / recovery" operation failed, then you can do almost no matter. You tried to recover, but you should continue without that the recovery operation is successful. Taking our instant messages as an example, a possible action order is: You have added a friend data to the database, but when you insert it into Friends_ Vector, you will fail, of course, you will try to delete it from the database. Although it is very likely that the data is deleted from the database, I don't know why it has failed. This situation is very annoying.
In general, you should use Guard in operations that ensure successful revocation.
The parameters supported by the packets are very happy to use ScopeGuard for a while, we have encountered a problem after the PETRU. Consider the following code:
Void Decrement (INT & X) {--X; Void Useresource (int REFCOUNT) { refcount; scopeguard guard = makeguard (Decrement, Refcount); ...}
The Guard object in the above code ensures that the value of Refcount remains unchanged when the Useresource function exits. (This usual method is useful in some shared resources.) Although useful, the above code cannot work. The problem is that ScopeGuard saves a copy of Refcount (see the definition of ScopeGuardImpl1, in member variable PARM_) instead of it is referenced. However, in this example, we need to save a reference to the Refcount, so that Decrement can operate it.
A solution is to reality Some classes, such as ScopeGuardImplref, and MakeguardRef. This will have a lot of duplication of labor, and this approach is difficult to deal with when implementing multi-parameter classes.
The way we take is to use an auxiliary class that transforms the reference to a value.
Template
Refholder and the auxiliary function of which it supports BYREF can be seamlessly adapted to the semantics of the value, and the scopeguardIMPL1 can be used without any changes. What you have to do is wrap the parameters in the reference form, like this: void decrement (int}) { refcount; scopeguard guard = makeguard (Decrement, Byref (refplace); ...}
We found this method very illustrative, which reminds you to deliver parameters in the reference method. This support reference method is the best in scopeguardimpl1 in Const modification. Here is the relevant code summary:
Template
This small Const is very important. It prevents code from using a non-Const reference through compilation and incorrectly. In other words, if you forget to use Byref, the compiler will not pass this error code. Wait a minute, there is still a little until now, you have a good tool to help you write the correct code, not worry. However, sometimes you want ScopeGuard to always execute when you exit a code block. In this case, it is very troublesome to define a scopeguard type. You only need a temporary variable without having to name it.
Macro ON_BLOCK_EXIT can do this, you can write this way better code: {file * TOPSecret = Fopen ("cia.txt"); on_block_exit (std :: fclose, topsecret); ... us TOPSecret .. .} // TOPSecret Automagically Closed
ON_BLOCK_EXIT said: "I hope to do this action when the current code block is exited." Similar, ON_BLOCK_EXIT_OBJ implements the same function for member function calls. These macros have used unsteady (although legitimate) tricks, this is not disclosed here. If you are curious, you can go to the code to view these macros (because the compiler's bug, friends with Microsoft VC must close "Program Database for Edit and Continue" setting, otherwise on_block_exit will have a problem).
SCOPEGUARD in reality we like ScopeGuard is because it is easy to use and conceptually. This article tells the entire implementation in detail, but to explain the use of ScopeGuard as long as a few minutes. ScopeGuard spread quickly like wildfire in the middle of our colleagues. Everyone thinks it is a very valuable tool, many cases help to prevent returning because of exceptions. With ScopeGuard, you can easily write unusually secure code, and it is also simple to understand and maintain.
Each tool has a recommended method, ScopeGuard is no exception. You should use it as SCOPEGUARD expectations - as an automatic variable in a function. You should not use the ScopeGuard object as a member variable or assign them on the heap. To this end, it is included in the downloaded code, which is the same as ScopeGuard, but taking more general practices - the cost is lost. Because of the compiler's bug, Borland C 5.5 users need Janitor instead of ScopeGuard.
Conclusion We discussed some situations that occur in writing an exception security code. After comparing several ways to obtain an abnormal safety in these cases, we introduced a method for preventing mistakes (and no throw) to revoke the operation available. ScopeGuard uses several generic programming techniques that allow you to specify functions and member functions called when ScopeGuard exit code blocks. As an option, you can also relieve the action of the ScopeGuard object.
When you need to implement the resource automatic release, you can rely on anti-wrong cancellation operations, ScopeGuard is very helpful to you in this case. When you put a few possibilities, you can also revoke the operations that form an atomic operation, which is very important. Of course, this method also does not apply.
Acknowledgments Herb Sutter conducted a special technical review in this paper. The author also thanked Mihai Antonescu and Dan PRAVAT to the amendments to this article and the proposals mentioned.
Reference [1] Bjarne Stroustrup. The C Programming Language, 3rd Edition (Addison-Wesley, 1997), Page 366.
[2] Herb Sutter. Exceptional C : 47 Engineering Puzzles, Programming Problems, And Solutions (Addison-Wesley. 2000).