Decoding Conditionals: A Dive into if-else, switch-case, Lookup Tables, and Interfaces

    powersagitar

    Have 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.

    Understanding if-else Statements

    if-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:

    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.

    c++
    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.

    Exploring switch Cases

    Now, 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.

    Note

    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:

    Embracing Lookup Tables

    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.

    c++
    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.

    The Power of Interfaces

    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:

    c++
    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:

    c++
    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:

    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.

    Summary

    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:

    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.