C++ Core Guidelines: More about Control Structures

Contents[Show]

My last German post C++ Core Guidelines: To Switch or not to Switch, that is the Question got a lot of attention. To use a hash table instead of a switch statement seems to be a highly emotional topic. So I change my original plan. Today, I will present different kinds of control structures. I will start with the if and switch statements, continue with the hash table, and end with dynamic and static polymorphism. Additionally, I will mark a few remarks about performance and maintainability. 

 640px Schienennetz Schweiz.svg

The classical control structure is the if statement; therefore, this is my starting point.

if statement

Here is the simple program that I will implement with different control structures.

// dispatchIf.cpp

#include <chrono>
#include <iostream>

enum class MessageSeverity{                                 // (2)
    information,
    warning,
    fatal,
};

auto start = std::chrono::steady_clock::now();              // (4)

void writeElapsedTime(){                                   
    auto now = std::chrono::steady_clock::now();            // (5)
    std::chrono::duration<double> diff = now - start;
  
    std::cerr << diff.count() << " sec. elapsed: ";
}

void writeInformation(){ std::cerr << "information" << std::endl; }
void writeWarning(){ std::cerr << "warning" << std::endl; }
void writeUnexpected(){ std::cerr << "unexpected" << std::endl; }

void writeMessage(MessageSeverity messServer){               // (1)
	
    writeElapsedTime();                                      // (3)
	
    if (MessageSeverity::information == messServer){
	    writeInformation();
    }
    else if (MessageSeverity::warning == messServer){
	    writeWarning();
    }
    else{
	    writeUnexpected();
    }
  
}

int main(){

    std::cout << std::endl;
  
    writeMessage(MessageSeverity::information);
    writeMessage(MessageSeverity::warning);
    writeMessage(MessageSeverity::fatal);

    std::cout << std::endl;

}

 

The function writeMessage in line (1)  displays the elapsed time in seconds (3) since the start of the program and a log message. It uses an enumeration (2) for the message severity. I use the start time (4) and the actual time (5) to calculate the elapsed time. As the name suggested, the std::steady_clock cannot be adjusted; therefore, it is the right choice for this measurement. The key part of the program is the part of the function writeMessage (1), in which I make the decision which message should be displayed. In this case, I used if-else statements. 

To be honest, I had to look up the syntax for the if-else statement to make it right. 

Here is the output of the program:

 dispatchIf

 I will skip the output for the remaining examples. Beside of the numbers, it is always the same.

switch statement

The following program is quite similar to the previous one. Only the implementation of the function writeMessage changed. 

// dispatchSwitch.cpp

#include <chrono>
#include <iostream>

enum class MessageSeverity{
    information,
    warning,
    fatal,
};

auto start = std::chrono::steady_clock::now();

void writeElapsedTime(){
    auto now = std::chrono::steady_clock::now();
    std::chrono::duration<double> diff = now - start;
  
    std::cerr << diff.count() << " sec. elapsed: ";
}

void writeInformation(){ std::cerr << "information" << std::endl; }
void writeWarning(){ std::cerr << "warning" << std::endl; }
void writeUnexpected(){ std::cerr << "unexpected" << std::endl; }

void writeMessage(MessageSeverity messSever){
	
    writeElapsedTime();

    switch(messSever){
        case MessageSeverity::information:
            writeInformation();
            break;
        case MessageSeverity::warning:
            writeWarning();
            break;
        default:
            writeUnexpected();
            break;
  }
  
}

int main(){

    std::cout << std::endl;
  
    writeMessage(MessageSeverity::information);
    writeMessage(MessageSeverity::warning);
    writeMessage(MessageSeverity::fatal);

    std::cout << std::endl;

}

I will make it short. Let's continue with the hash table.

Hashtable

For a more elaborate discussion of the switch statement and the hash table, read my last post: C++ Core Guidelines: To Switch or not to Switch, that is the Question.

 

// dispatchHashtable.cpp

#include <chrono>
#include <functional>
#include <iostream>
#include <unordered_map>

enum class MessageSeverity{
  information,
  warning,
  fatal,
};

auto start = std::chrono::steady_clock::now();

void writeElapsedTime(){
    auto now = std::chrono::steady_clock::now();
    std::chrono::duration<double> diff = now - start;
  
    std::cerr << diff.count() << " sec. elapsed: ";
}

void writeInformation(){ std::cerr << "information" << std::endl; }
void writeWarning(){ std::cerr << "warning" << std::endl; }
void writeUnexpected(){ std::cerr << "unexpected" << std::endl; }

std::unordered_map<MessageSeverity, std::function<void()>> mess2Func{
    {MessageSeverity::information, writeInformation},
    {MessageSeverity::warning, writeWarning},
    {MessageSeverity::fatal, writeUnexpected}
};

void writeMessage(MessageSeverity messServer){
	
	writeElapsedTime();
	
	mess2Func[messServer]();
	
}

int main(){

  std::cout << std::endl;
  
  writeMessage(MessageSeverity::information);
  writeMessage(MessageSeverity::warning);
  writeMessage(MessageSeverity::fatal);

  std::cout << std::endl;

}

Is this the end? No? In C++ we have dynamic and static polymorphism a few of my readers mentioned in their discussion. With the if-else or the switch statement, I used enumerator for dispatching to the right case. The key of my hash table behaves in a similar way. 

Dynamic or static polymorphism is totally different. Instead of an enumerator or a key for dispatching to the right action, I use objects which decide autonomously at runtime (dynamic polymorphism) or compile time (static polymorphism) what should be done. 

Let's continue with dynamic polymorphism.

Dynamic polymorphism

Not, the decision logic is encoded in the type hierarchy.

// dispatchDynamicPolymorphism.cpp

#include <chrono>
#include <iostream>

auto start = std::chrono::steady_clock::now();

void writeElapsedTime(){
    auto now = std::chrono::steady_clock::now();
    std::chrono::duration<double> diff = now - start;
  
    std::cerr << diff.count() << " sec. elapsed: ";
}

struct MessageSeverity{                         // (1)
	virtual void writeMessage() const {
		std::cerr << "unexpected" << std::endl;
	}
};

struct MessageInformation: MessageSeverity{     // (2)
	void writeMessage() const override {
		std::cerr << "information" << std::endl;
	}
};

struct MessageWarning: MessageSeverity{         // (3)
	void writeMessage() const override {
		std::cerr << "warning" << std::endl;
	}
};

struct MessageFatal: MessageSeverity{};

void writeMessageReference(const MessageSeverity& messServer){
	
	writeElapsedTime();
	messServer.writeMessage();
	
}

void writeMessagePointer(const MessageSeverity* messServer){
	
	writeElapsedTime();
	messServer->writeMessage();
	
}

int main(){

    std::cout << std::endl;
  
    MessageInformation messInfo;
    MessageWarning messWarn;
    MessageFatal messFatal;
  
    MessageSeverity& messRef1 = messInfo;            
    MessageSeverity& messRef2 = messWarn;
    MessageSeverity& messRef3 = messFatal;
  
    writeMessageReference(messRef1);              // (4)
    writeMessageReference(messRef2);
    writeMessageReference(messRef3);
  
    std::cerr << std::endl;
  
    MessageSeverity* messPoin1 = new MessageInformation;
    MessageSeverity* messPoin2 = new MessageWarning;
    MessageSeverity* messPoin3 = new MessageFatal;
  
    writeMessagePointer(messPoin1);               // (5)
    writeMessagePointer(messPoin2);
    writeMessagePointer(messPoin3);
  
    std::cout << std::endl;

}

 

The classes (1), (2), and (3) know, what the have to display if used. The key idea is that the static type MessageSeverity differs from the dynamic type such as MessageInformation(4); therefore, the late binding will kick in and the writeMessage methods (5), (6), and (7) of the dynamic types are used. Dynamic polymorphism requires a kind of indirection. You can use references (8) or pointers (9). 

From a performance perspective, we can do better and make the dispatch at compile time.

Static polymorphism  

Static polymorphism is often called CRTP. CRTP stands for c++ idiom Curiously Recurring Template Pattern. Curiously because a class derives in this technique from a class template instantiation using itself as template argument.

// dispatchStaticPolymorphism.cpp

#include <chrono>
#include <iostream>

auto start = std::chrono::steady_clock::now();

void writeElapsedTime(){
    auto now = std::chrono::steady_clock::now();
    std::chrono::duration<double> diff = now - start;
  
    std::cerr << diff.count() << " sec. elapsed: ";
}

template <typename ConcreteMessage>                        // (1)
struct MessageSeverity{
  void writeMessage(){                                     // (2)
    static_cast<ConcreteMessage*>(this)->writeMessageImplementation();
  }
  void writeMessageImplementation() const {
    std::cerr << "unexpected" << std::endl;
  }
};

struct MessageInformation: MessageSeverity<MessageInformation>{
  void writeMessageImplementation() const {               // (3)
    std::cerr << "information" << std::endl;
  }
};

struct MessageWarning: MessageSeverity<MessageWarning>{
  void writeMessageImplementation() const {               // (4)
    std::cerr << "warning" << std::endl;
  }
};

struct MessageFatal: MessageSeverity<MessageFatal>{};     // (5)

template <typename T>
void writeMessage(T& messServer){                       
	
    writeElapsedTime();                                   
    messServer.writeMessage();                            // (6)
	
}

int main(){

    std::cout << std::endl;
  
    MessageInformation messInfo;
    writeMessage(messInfo);
    
    MessageWarning messWarn;
    writeMessage(messWarn);
	
    MessageFatal messFatal;
    writeMessage(messFatal);
  
    std::cout << std::endl;

}

In this case, all concrete classes (3), (4), and (5) derive from the base class MessageSeverity. The method writeMessage is a kind of an interface that dispatches to the concrete implementations writeMessageImplementation.  To make that happen, the object will be upcasted to the ConcreteMessage static_cast<ConcreteMessage*>(this)->writeMessageImplementation();. This is the static dispatch at compile time; therefore, this technique is called static polymorphism.

To be honest, I took me a time to get used to it, but the applying of the static polymorphism in line (6) is quite easy. If the curiously recurring template pattern is still curious to you, I wrote an article about it: C++ is doch lazy!

To end my comparison, let me compare this variious techniques.

My simple comparison

Let's first look at your prefered way to implement and maintain a control structure. Depending on your experience as a C programmer, the if or switch statements seem quite natural to you. If you have an interpreter background, you may prefer the hash table. With an object orientation background, the dynamic polymorphism is your prefered way to implement the control structure. The static polymorphism, also called CRTP, is quite special; therefore, it will take some time to get comfortable with it. Afterwards it quite a pattern you have to use. 

From the security perspective, I have to mention the new context-sensitive identifiers override.  It helps to express your intent to override a virtual method in your type hierarchy. If you will make it wrong, the compiler will complain. 

Now to the more interesting question. What are the performance differences? I will only provide a rough idea without numbers. If you have a long series of if statements, this will become quite expensive because a lot of comparisons are involved. The dynamic polymorphism and the hash table will be faster and in the same ballpark, because in both cases a pointer indirection is involved. The switch statement and the static polymorphism make their decision at compile time; therefore, they are the two fastest control structures.

What's next?

I hope, I'm done with the discussion of the different control structures; therefore, I will in my next post the last rules to statements and start with the rules to arithmetic expressions.

 

 

Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, Marko, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Darshan Mody, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Animus24, Jozo Leko, John Breland, espkk, Wolfgang Gärtner,  Louis St-Amour, Stephan Roslen, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Avi Kohn, Robert Blanch, Truels Wissneth, Kris Kafka, Mario Luoni, Neil Wang, Friedrich Huber, Sudhakar Balagurusamy, lennonli, and Pramod Tikare Muralidhara.

 

Thanks in particular to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, and Dendi Suhubdy

 

Seminars

I'm happy to give online-seminars or face-to-face seminars world-wide. Please call me if you have any questions.

Bookable (Online)

Deutsch

English

Standard Seminars 

Here is a compilation of my standard seminars. These seminars are only meant to give you a first orientation.

New

Contact Me

Modernes C++,

RainerGrimmSmall

My Newest E-Books

Course: Modern C++ Concurrency in Practice

Course: C++ Standard Library including C++14 & C++17

Course: Embedded Programming with Modern C++

Course: Generic Programming (Templates)

Course: C++ Fundamentals for Professionals

Subscribe to the newsletter (+ pdf bundle)

Blog archive

Source Code

Visitors

Today 2847

Yesterday 7709

Week 26943

Month 185374

All 5054688

Currently are 153 guests and no members online

Kubik-Rubik Joomla! Extensions

Latest comments