if-else, switch-case, Lookup Tables, and InterfacesHave you ever been confused about when to use if-else statements, switch cases, or other conditionals in your code? This article may help you to clarify your thoughts.
if-else Statementsif-else statements are fundamental constructs in nearly all programming languages. But when should we prefer them? Let’s explore their semantic meanings first before considering performance implications:
if blocks are executed only when condition evaluates to true.else if blocks are executed only when condition evaluates to true and all preceding if or else if conditions have evaluated to false.else blocks serve as a fallback or default case, executed when no preceding conditions have evaluated to true.From this, we can observe that if-else statements are designed to handle branching logic that requires evaluation of conditions. More specifically, they handle conditions that have a hierarchical relationship, as else if statements are equivalent to nested if statements within else blocks.
if (condition1) {
// Your magic
} else {
if (condition2) {
// Your other magic
}
}In terms of performance, modern processors employ branch prediction mechanisms. However, when these mechanisms fail to guess the correct branch, the program would suffer a significant performance penalty, leading to increased time complexity.
Therefore, we can conclude that if-else statements may not be the optimal choice in scenarios where conditions do not require a fallback logic.
switch CasesNow, let’s delve into switch cases.
Once again, think about switch's semantic meaning first. The switch statement evaluates a given expression and, based on the evaluation, it executes the corresponding case. switch statements provide an easy way to dispatch execution to different parts of code based on different conditions.
We have to agree that switch cases may work similar to if statements under the hood, both generating a lookup table for the conditions, when they are simple and involve checking a variable against a range of values. However, it’s also worth noting that this type of optimization is highly dependent on the specific compiler and the nature of the conditions, and is more commonly associated with switch statements.
gcc -O3 (GCC) enables nearly all optimizations that don’t involve a space-speed tradeoff.
cl /O2 (MSVC) enables optimizations for maximum speed.
For instance, x86-64 gcc 13.2 conducts such optimization when the flag is set to -O3.
While x86 msvc v19.38 doesn’t, when the flag is set to /O2.
In general, it’s best to choose between these two based on their semantic meanings, and let the compiler handle the optimizations:
if statements are best used when the branches have hierarchical relationships.switch-cases are ideal when the branches don’t have hierarchical relationships, and different actions are taken based on different conditions but not in a fallback manner.As observed, the lookup table derived from switch-cases boasts an O(1) time complexity, which is ideal. However, not all types of variables can be used in the condition of a switch statement. For instance, in C++, the condition only accepts integral and enumeration types, as well as types that can be implicitly converted to those two, as stated in the C++ reference. So, what happens when this is the case?
The solution is quite straightforward — if the compiler doesn’t generate a lookup table, we implement it ourselves. The following snippet demonstrates how to use a lookup table in conjunction with std::unordered_map.
int Handler1(int argc, char **argv) {}
int Handler2(int argc, char **argv) {}
int main(int argc, char **argv) {
std::unordered_map<std::string_view, int (*)(int, char**)> lookup_table = {
{"command1", Handler1},
{"command2", Handler2},
};
if (argc < 2) {
return EX_USAGE;
}
try {
int error_code = lookup_table.at(argv[1])(argc, argv);
// Handle the error code
} catch (std::out_of_range &exception) {
// Handle the exception
return EX_USAGE;
}
return EX_OK;
}In C++, std::unordered_map is typically implemented as a hash table and thereby offering an average time complexity of O(1), which is fundamentally identical to the lookup table generated by the compiler.
Thus, custom lookup tables appear to be a viable alternative to switch-cases where they are not applicable.
We have explored various conditions so far and have observed that switch-cases and lookup tables can be effective when dealing with a large number of branches. However, they are not a panacea. Consider scenarios where:
In such cases, interfaces come into play. As a reminder, by the Wikipedia contributors:
An interface or protocol is a data type that acts as an abstraction of a class. It describes a set of method signatures, the implementations of which may be provided by multiple classes that are otherwise not necessarily related to each other.
Let’s see how interfaces can help with an example.
Instead of this:
enum class HandlerType {
Handler1,
Handler2,
} handler_type = HandlerType::Handler1;
void Execute(int some_arg) {
switch (handler_type) {
case HandlerType::Handler1:
// Your magic
break;
case HandlerType::Handler2:
// Your other magic
break;
}
}Consider this:
class Interface {
public:
virtual ~Interface() = default;
virtual void Execute(int some_arg) = 0;
};
class Handler1 : public Interface {
public:
void Execute(int some_arg) override {
// Your magic
}
};
class Handler2 : public Interface {
public:
void Execute(int some_arg) override {
// Your other magic
}
};
void CallExecute(Interface &handler, int some_arg) {
handler.Execute(some_arg);
}
int main() {
Handler1 handler1;
CallExecute(handler1, 1);
return EX_OK;
}The benefits are clear:
switch and cases are eliminated, making the code more readable and maintainable.case.Therefore, in scenarios where multiple types of objects share a common interface but require different actions, this approach is more suitable and scalable than using if-else or switch-cases. This is also a fundamental technique in object-orientated programming.
In conclusion, there is no universal solution when it comes to choosing conditionals. The most suitable option always depends on the specific circumstances at hand. Here’s a guide for quick reference:
if-else statements when the branches have hierarchical relationships.switch-cases and lookup tables when the branches don’t have hierarchical relationships, and different actions are taken based on different conditions, but not in a fallback manner.Remember, the key is to consider the specific situation and choose the most appropriate means.
Thank you for taking your time to read! If you have any questions, concerns, or if you spot any issues, please feel free to connect with powersagitar. Your feedback is greatly appreciated.