Dynamic and Static dispatch in C++
Do not miss this exclusive book on Binary Tree Problems. Get it now for free.
A dispatch mechanism determines which function gets executed when called upon by an object. In this OpenGenus article, we will explore each of these mechanisms along their approach to overloading and polymorphism.
Table of contents:
- Overview
- Virtual functions and vtables
- Static dispatch
- Dynamic dispatch
- Linking design patterns
- Conclusion
Overview
-
Static dispatch, also known as early binding or compile-time binding, is a technique where the decision about which function to call is made at compile time based on the type of the object.
-
The compiler determines the corresponding function to execute based on the type of the reference or pointer, and this decision remains fixed throughout the program's execution.
-
Static dispatch is commonly employed for non-virtual functions and functions invoked on objects with static (known at compile-time) types.
-
Dynamic dispatch, also referred to as late binding or run-time binding determines which function to call at runtime, and it is resolved based on the dynamic type of the object.
-
The decision about which function is invoked is made during program execution. Dynamic dispatch is achieved through virtual functions, which are functions declared in a base class and overridden in derived classes.
Virtual functions and vtables
- When a function is declared in the base class using the keyword 'virtual', it can be overriden by a derived class. This overriding is done at run-time and exhibits a polymorphic behaviour, where a pointer or reference of a base class type can be used to invoke a function specific to a derived class, depending on the actual type of the object being pointed to or referenced.
- vtables are generated by the compiler, for each class where virtual functions are used. It is an array of functions pointers, where each entry corresponds to a virtual function in the class. Every object of a class with virtual functions, further contains a vptr which points to the corresponding vtable.
Therefore, when a virtual function is called through a base pointer or reference, the vptr is used to determine the function to be called from the vtable at runtime.
In the above diagram, the virtual function implemented by the base class stores a vtable and the object of base class stores a vptr to access the vtable. The vtable points to the function implementation to be called based on type of object. If the base object points to derived class, the derived class implementation is called. If it points to the base class type, the base class function implementation is called.
Static Dispatch
The static dispatch exhibits polymorphic behavior when dealing with function overloading and operator overloading.
1. Function overloading
Function overloading allows multiple functions with the same name but different parameter lists to coexist within the same scope. The appropriate function call is determined based on the number and/or types of the arguments, and the resolution is done at compile time.
return_type function_name(type1 arg1, type2 arg2, ...);
return_type function_name(type3 arg1, type1 arg2, ...);
In the example shown below,
- foo functions are overloaded with and without an integer parameter, and bar with a double parameter.
- In the main function, we call the foo function twice; once without any arguments, and the second time with an integer argument. The two functions are bound accordingly at compile time.
We call the bar function with a double argument, which is also resolved at compile time.
#include <iostream>
class Example
{
public:
void foo()
{
std::cout<<"Calling foo() without arguments"<<std::endl;
}
void foo(int x)
{
std::cout<<"Calling foo() with argument: "<<x<<std::endl;
}
void bar(double y)
{
std::cout<<"Calling bar() with argument: "<<y<<std::endl;
}
};
int main()
{
Example obj;
obj.foo(); // Calls foo() without arguments
obj.foo(10); // Calls foo() with argument: 10
obj.bar(3.14); // Calls bar() with argument: 3.14
return 0;
}
2. Operator overloading
It is a feature that allows you to redefine the behavior of operators for user-defined types. In other words, you can define how operators behave when applied to objects of your own classes.
return_type operator<operator_to_overload>(type1 arg1, type2 arg2, ...);
Below, We have a class Example:
- We overload the "+" operator using a member function. Inside the function, we perform addition of the member variables of two Example objects and return a new Example object with the result.
- In the main function, we create two Example objects obj1 and obj2 with values 5 and 10, respectively.
- We use the overloaded + operator to add obj1 and obj2, and store the result in the result variable. Finally, we call the display function to output the result of the addition.
#include <iostream>
class Example
{
private:
int value;
public:
Example(int val) : value(val) {}
//Overload operator +
Example operator+(const Example& other)
{
return Example(this->value + other.value);
}
void print()
{
std::cout<<"Value: "<<value<<std::endl;
}
};
int main()
{
Example obj1(5);
Example obj2(10);
Example result = obj1 + obj2; // Calls the overloaded + operator
result.print();
return 0;
}
Note: In the scenario given below, We have a Base class with a print function and a Derived class inheriting from Base with its own print function.
- In the main function, we create objects of both the Base and Derived classes.
We also create a pointer basePtr of type Base* pointing to a Derived object. - When calling the print function using objects baseObj and derivedObj, the function call is resolved at compile time based on the static type of the object, resulting in static binding.
- Similarly, when calling the print function using the basePtr pointer, the function call is resolved based on the static type of the pointer, resulting in static binding as well.
#include <iostream>
class Base
{
public:
void print()
{
std::cout<<"Base::print() called"<<std::endl;
}
};
class Derived : public Base
{
public:
void print()
{
std::cout<<"Derived::print() called"<<std::endl;
}
};
int main()
{
Base baseObj;
Derived derivedObj;
Base* basePtr = &derivedObj; //base pointer pointing to derived object type
baseObj.print(); //Calls Base::print()
derivedObj.print(); // Calls Derived::print()
basePtr->print(); //Calls Base::print()
return 0;
}
Dynamic Dispatch
In C++, dynamic dispatch is achieved through virtual functions, which enable late binding or runtime binding. The syntax for dynamic dispatch involves declaring virtual functions in base classes and overriding these functions in derived classes.
1. Virtual function definition:
In the base class, declare a virtual function by using the virtual keyword in the function declaration.
This indicates to the compiler that this function can be overridden by derived class.
class Base
{
public:
virtual void virtualFunction()
{
//Function definition
}
};
2. Overriding the Virtual Function:
In the derived class, override the base class function.
class Derived : public Base
{
public:
void virtualFunction() override
{
// Derived function definition
}
};
3. Dynamic function calls:
Create objects of both the base and derived classes and call the virtual function on the base class pointer or reference.
The function call will be determined at runtime depending on the actual type of the address the base pointer stores.
int main()
{
Base* basePtr = new Derived(); //Base pointer to derived object
basePtr->virtualFunction(); //Calls Derived::virtualFunction()
delete basePtr;
return 0;
}
Let's delve into dynamic dispatch with a detailed example:
- A Base pointer, basePtr is created and assigned the address of the derivedObj.
- Since print() is a virtual function in the Base class and is overridden in the Derived class, the actual function called is determined by the dynamic type of the object being pointed to.
- The dynamic type of the object being pointed to is Derived, so the call to print() resolves to Derived::print().
#include <iostream>
class Base
{
public:
virtual void print()
{
std::cout<<"Base::print() called"<<std::endl;
}
};
class Derived : public Base
{
public:
void print() override
{
std::cout << "Derived::print() called"<<std::endl;
}
};
int main()
{
Derived derivedObj;
Base* basePtr = &derivedObj; //Pointer of type Base pointing to a Derived object
basePtr->print(); //Calls Derived::print() dynamically at runtime
return 0;
}
Pros and Cons of Dynamic dispatch
Pros :
- Flexibility: Different object types can be accessed through common interface.
- Extensibility: Addition of functionality to existing classes.
- Polymorphism: Objects respond differently to the same method call, promoting code reuse.
- Late Binding: Method calls are resolved dynamically at runtime, enabling dynamic behavior.
- Encapsulation: Hides implementation details.
Cons :
- Performance Overhead: Runtime checks can impact program efficiency.
- Less Predictable Execution: Behavior may not be known at compile time, leading to uncertainty.
- Increased Complexity: Runtime behavior of polymorphic objects adds complexity to codebase.
- Runtime Errors: Mistakes in implementation can lead to runtime errors.
- Difficulty in Optimization: Challenges in program optimization for compilers.
Name mangling
- The role of name mangling in static and dynamic dispatch is crucial since it enables these techniques. Name mangling is used by compilers to handle function overloading and virtual function calls.
- The compiler resolves the correct function to call by encoding additional information into the function names. This encoding allows the compiler to differentiate between each function based on parameter lists and resolve virtual function calls correctly at runtime.
- The additional information includes function parameter types and return type, class name and other compiler-specific details.
- When virtual functions overridden in the derived classes, the compiler generates unique mangled names for each overridden function. These mangled names contain information about the function's signature, class, and other relevant details.
- During runtime, when a virtual function is called, the compiler uses the mangled name to resolve the correct function based on the actual type of the object. This process ensures that the correct overridden function is invoked, enabling polymorphic behavior in C++ programs.
Linking design patterns
The visitor behavioural pattern can be used to employ dispatch mechanisms to add new operations to existing object structures without modifying those structures.
It achieves this by separating the algorithm from the objects on which it operates.
One of the distinguishing features of the Visitor Pattern is its ability to achieve dynamic linking, typically through double dispatch.
Visitor patter terminology:
- Visitor: The visitor class itself, which defines the interface for visiting different types of objects in the object structure.
- Visitor Methods: These are methods are default methods for each element.
- Concrete Visitor: A subclass of the visitor class that implements the visit methods to define specific operations of an object.
- Element: The objects of a class.
- Concrete Element: A subclass of the object class that represents a specific type of object. Each concrete element implements the accept() method to allow the visitor to visit it.
- Accept Method: A method defined in the element class that allows a visitor to visit the element. This method calls the visit method on the visitor, passing the visitor as an argument.
- Double dispatch : This mechanism is used to dynamically handle method calls based on the types of two objects involved in a function/operation.
Here's how double dispatch works in the Visitor Pattern:
- The element invokes a method on the visitor object by passing itself as an argument.
- The visitor class, upon receiving the element, performs another method call on the element object, passing itself as an argument.
- The method to be executed is determined by the combination of the actual types of both the visitor and the element being visited.
It is useful when dealing with objects where a combination of types is involved, rather than the type of visitor alone. This makes it easier to add new functions to the classes without modifying existing code.
Example,
#include <iostream>
#include <string>
//Forward declaration of visitor class to avoid circular dependency issues
class Doll;
class Robot;
// Visitor interface : operations to be performed by different visitors is mentioned here
class ToyVisitor
{
public:
//operations are based on types of object
virtual void visit(Doll *) = 0;
virtual void visit(Robot *) = 0;
};
// Concrete visitor1
class ToyVisitor1 : public ToyVisitor
{
public:
void visit(Doll *doll) override
{
std::cout << "(Visitor 1) is assembling Doll number 1"<<std::endl;
}
void visit(Robot *robot) override
{
std::cout << "(Visitor 1) is playing with Robot number 1"<<std::endl;
}
};
// Concrete visitor2
class ToyVisitor2 : public ToyVisitor
{
public:
void visit(Doll *doll) override
{
std::cout << "(Visitor 2) is playing with Doll number 2"<<std::endl;
}
void visit(Robot *robot) override
{
std::cout << "(Visitor 2) is assembling Robot number 2"<<std::endl;
}
};
// Component interface : different types of toys(elements) will implement the accept method
class Toy
{
public:
//each toy accepts different types of visitors(clients)
virtual void accept(class ToyVisitor *) = 0;
};
// Concrete Doll class
class Doll : public Toy
{
public:
void accept(ToyVisitor *dollVisitor) override
{
dollVisitor->visit(this);
}
};
// Concrete Robot class
class Robot : public Toy
{
public:
void accept(ToyVisitor *robotVisitor) override
{
robotVisitor->visit(this);
}
};
// Client code
int main()
{
Doll doll;
Robot robot;
ToyVisitor1 visitor1;
ToyVisitor2 visitor2;
std::cout << "Using ToyVisitor1:"<<std::endl;
doll.accept(&visitor1);
robot.accept(&visitor1);
std::cout << "\nUsing ToyAssemblyVisitor2:\n";
doll.accept(&visitor2);
robot.accept(&visitor2);
return 0;
}
Similarly, in static dispatch, dynamic linking can be performed at compile-time using CRTP(Curiously Recurring Template Pattern).
- The CRTP is an idiomatic C++ pattern that utilizes templates and inheritance to achieve static polymorphism. A base class template takes a derived class as a template argument.
- The derived class inherits from the base class. This allows the base class to access members of the derived class through static casting.
- CRTP achieves polymorphism at compile-time rather than runtime, as there are no virtual function calls involved.
In the below code snippet, compile-time linking using static_cast helps reduce the run-time overhead associated with dynamic dispatch.
#include <iostream>
// Base class template using CRTP
template <typename Derived>
class Toy
{
public:
//common method for all derived classes
void assembleToy()
{
std::cout << "Assembling a toy"<<std::endl;
}
//Method to be implemented by derived classes
void assemble()
{
// Access Derived class's members and methods
static_cast<Derived*>(this)->accept();
}
};
//Derived class inherits from Toy class using CRTP
class Doll : public Toy<Doll>
{
public:
// Implementation specific to Doll class
void assemble()
{
std::cout << "Assembled a doll"<<std::endl;
}
};
//Derived class inheriting from Toy class using CRTP
class Robot : public Toy<Robot>
{
public:
// Implementation specific to Robot class
void assemble()
{
std::cout << "Assembled a robot"<<std::endl;
}
};
int main()
{
Doll doll;
Robot robot;
doll.assembleToy(); // Access common functionality from Toy class
doll.assemble(); // Access Doll implementation using CRTP
robot.assemble(); // Access Robot implementation using CRTP
return 0;
}
Conclusion
In conclusion, while static dispatch offers predictability and better performance by resolving function calls at compile time, dynamic dispatch provides run-time flexibility and polymorphism and they are both fundamental aspects of object-oriented design, crucial for more maintainable, modular, and extensible software systems.
Sign up for FREE 3 months of Amazon Music. YOU MUST NOT MISS.