Optimization skills of multi-level if/case statements in software development

In daily development, we often see if statements nested in if. Once I had an exchange with a senior coder. Most coders wrote business codes as long as they knew if else. He said that it may not be so. When there are a large number of if else nesting, we can see different code levels.

  • There are a lot of if else nesting, which is difficult to read, and bug s should be analyzed according to the logic of if. It's too tired.
  • There are too many levels of if else nesting. You need to understand the business relationship of each level before you can reach the core code.

Case 1: merge if condition

if(conditionA())
{
      if(conditionB())
     {
           func();
     }
}
//The above code can be optimized as follows:
if(conditionA() && conditionB())
      func();

Because of this optimization, if condition a is not satisfied, it will not enter the following function condition B. the effect is the same, but it does reduce the nesting level of if and has better readability.

Case 2: error early return

The above case is relatively simple, and there is no big problem from the perspective of code cleanliness. Let's take a more complex example, the case of optimization in actual development.

if(conditionA())
{
    if(conditionB())
    {
        if(confitionC())
       {
          funB();
       }
   }
   else
      funC();
}
//Such cases can be optimized as follows:
if(!conditionA())
    return ;
if(!conditionB())
{
    funC();
    return;
}
if(conditionC())
    funB();

The refactoring methods of the above codes are called Guard Clauses, which is a very useful code refactoring technique. This refactoring optimization method tries to let the wrong ones return first, so that clean code will be obtained later.

Case 3: improving branch predictability

In addition, in order to improve the accuracy of branch prediction of if statements, we can use like and un likely statements. Let's take the following example to illustrate.

if(nullptr == pManager)
   return ;
else
   funA();

In the above case, we make non nullptr judgment on the pointer object, which only increases the robustness of the code. In fact, it will not be nullptr most of the time, that is, if the probability is not satisfied, then we can improve the effect of the compiler on branch prediction. We can optimize as follows:

if(unlikely(nullptr == pManager))
    return ;
else 
    funA();
//Or use like to improve the accuracy of branch prediction
if(likely(nullptr != pManager))
    funA();
else
   return;

Case 4: split function optimization

In addition, in our daily development, there are many judgments on the status and return value, which will involve a large number of if statements. Such code reads around and is not very friendly to code maintenance. We can divide the functions and package them into functions for optimization.

int Init()
{
    int rc = 0;
    if(nullptr == pContext)
        pContext = new Context();
    if(nullptr == pContext)
       return -1;
    rc = pContext->Init();
    if(rc < 0)
        return rc ;
    if(nullptr == pManager)
        pManager = new CManager();
    if(nullptr == pManager)
        return -2;
     rc = pManager->Init(); 
     if(rc < 0)
         return rc;
     return 0;  
}

The above code does two things: initialize the Context object. If it is empty, create the object and initialize it. Secondly, judge whether the cmmanager object is empty. If it is empty, create the object and perform initialization. Finally, return 0 to indicate successful initialization, otherwise initialization fails. For this code, we can split it into two functions according to the function, namely Init_Ctx() and Init_Manager(); Reference codes are as follows:

int Init_Ctx()
{
   if(nullptr == pContext)
       pContext = new Conetxt();
   if(nullptr == pContext)
      return -1;
   return pContext->Init();
}

int Init_Manager()
{
   if(nullptr == pManager)
        pManager = new CManager();
   if(nullptr == pManager)
        return -2;
    return pManager->Init(); 
}

Finally, the Init function is implemented as follows:

int Init()
{
   if(Init_Ctx() < 0 )
      return -1;
   if(Init_Manager() < 0 )
      return -2;
   return 0;
}

If the pManager and pContext objects in the above example do not require the order of initialization, they can be more concise. The reference code is as follows:

int Init()
{
   return (Init_Ctx() || Init_Manager());
}

As can be seen from the above code, after module splitting, the code is cleaner and more beautiful, and the function is clearer and more intuitive to read.

Case 5: merge the same functions and delegate

Here, I think of the function of processing network messages in a previous project. The original design reference is as follows:

//Business thread 1: processing center application server messages
void ProcessNetMsg(char* data,unsigned short len)
{
    short id =  ParseBuffer(data,len);
    switch(id)
    {
        case ID_MSG_1:
             Process_Msg_1(data,len);
             break;
        case ID_MSG_2:
             Process_Msg_2(data,len);
             break;
        .....
        default: break;
    }
}
void Process_Msg_1(char* data,unsigned short len)
{
     //1. Parse message parse message
     .......
     //2. Processing business 
     .....
}

void Process_Msg_1(char* data,unsigned short len)
{
     //1. Parse message parse message
     .......
     //2. Processing business 
     .....
}

The code of business thread 2 is similar to that of 1, as shown below.

//Business thread 2: Processing regional application server messages
void ProcessNetMsg(char* data,unsigned short len)
{
    short id =  ParseBuffer(data,len);
    switch(id)
    {
        case ID_MSG_7:
             Process_Msg_1(data,len);
             break;
        case ID_MSG_8:
             Process_Msg_2(data,len);
             break;
        ......
        default: break;
    }
}
void Process_Msg_7(char* data,unsigned short len)
{
     //1. Parse message parse message
     .......
     //2. Processing business 
     .....
}

void Process_Msg_8(char* data,unsigned short len)
{
     //1. Parse message parse message
     .......
     //2. Processing business 
     .....
}

As can be seen from the above, both business thread 1 and business thread 2 have duplicate code, that is, parsing the message package and then processing the business logic. From a functional point of view, parsing data is a business independent process, and the ProcessNetMsg of these business threads can be merged. Then we can add a class CNetDataParse to parse messages to get business data objects. Reference codes are as follows:

//Class CNetDataParse class
void ProcessNetMsg(void* ptr, char* data, unsigned short len)
{
    short id = ParseBuffer(data,len);
    switch(id)
    {
        case MSG_ID_1:
            Process_MSG_1(ptr,data,len);
            break;
        case MSG_ID_2:
            break;
         ....
        case MSG_ID_7:
            break;
        case MSG_ID_8:
             break;
        default: break;
    }
}

void Process_MSG_1(void* ptr,char* data, unsigned short len)
{
    struct MSG_1_STU stu_1;
    ....
    .....The message parsing process is omitted....
    ....
    //Delegate calls method processing in the business thread  
    Thread1Service* service = (Thread1Service*)ptr;
    ptr->Process_Msg_1(stu_1);
}

Finally, let's look at how the code of the original business class is simplified and called.

void ProcessNetMsg(char* data,unsigned short len)
{
    CNetDataUtil::ProcessNetMsg(this,data,len);
}

void Process_Msg_1(struct MSG_1_STU& data)
{
    .....Omit business code 
}

The advantage of the above is that in the CNetDataParse class, you only need to parse the data to get the business data, and then delegate it to the methods in different objects to process the business data. In this way, processing network message packets in the service thread is decoupled from processing services.

In fact, the above method can also register different business classes during initialization in CNetDataParse, which can further optimize the code. The idea of its implementation is to delegate the message to other classes for processing, and then call back the processing functions in different business classes to complete the processing.

We need to know the fact that methods in this class may also be called in other classes.

Keywords: C++ Software development

Added by cvsherri on Fri, 31 Dec 2021 01:49:45 +0200