Search code examples
c++c++11exceptionreturn-type

Why is throwing exceptions not considered a technique to return different data types at runtime?


I've this in mind:

// This is called at multiple locations and each handles the returned object
// differently. Handling also differs depending on the returned object type.
void GetObject() {
    // Check runtime condition

    // Object1 and Object2 are completely dissimilar and do not share a base class.
    if (condition1) {
        // Operations to prepare for construction of Object1
        throw Object1{ var1, var2, var3 };
    }
    if (condition2) {
        // Operations to prepare for construction of Object2
        throw Object2{ var4, var5 };
    }
    throw MyException{};
}

// Usage example
int main() {
    try {
        GetObject();
    }
    catch (const Object1& obj1) {
        // Object1-specific handling
    }
    catch (const Object2& obj2) {
        // Object2-specific handling
    }
    catch (const MyException& e) {
        // Error handling
    }
}

Curiously, the answers to questions about returning different data types do not mention this technique at all:

Is there any particular reason for this? The only reason I can think of is that it's unorthodox, otherwise this seems like the simplest and cleanest approach - no 3rd party libraries, no need to upgrade to C++17, no additional abstractions.

Update

As many have pointed out in the comments, std::variant would solve the problem nicely, but it requires C++17 or the boost library; unfortunately my project is unable to use either at the moment.

Update 2

Thanks everyone for your inputs. They were very helpful in making me think through the various design options and highlight areas where I overlooked. I've selected the answer below as it directly addresses the reason why this technique is not considered for use. The other 2 answers proposed alternative techniques, so do take a look at them if you have similar use cases. For me, I eventually decided on output parameters as the solution due to its simplicity and also my constraints of no C++17 and no boost:

bool GetObject(
    std::unique_ptr<Object1>& obj1,
    std::unique_ptr<Object2>& obj2) {
    // Check runtime condition

    if (condition1) {
        // Operations to prepare for construction of Object1
        obj1 = std::make_unique<Object1>(var1, var2, var3);
        return true;
    }
    if (condition2) {
        // Operations to prepare for construction of Object2
        obj2 = std::make_unique<Object2>(var4, var5);
        return true;
    }
    return false;
}

// Usage example
int main() {
    std::unique_ptr<Object1> obj1;
    std::unique_ptr<Object2> obj2;
    if (!GetObject(obj1, obj2)) {
        // Error handling
        return -1;
    }
    if (obj1) {
        // Object1-specific handling
    }
    else {
        // Object2-specific handling
    }
    ...
}

Instead of unique_ptr, statically allocating the objects (i.e. on the stack) will also work if the objects aren't too large. But this requires Object1 and Object2 to have member functions that can tell callers whether the class has been initialized.


Solution

  • Exceptions are too costly for polymorphism

    Exceptions are simply the wrong mechanism for polymorphism. Throwing an exception is an extremely expensive operation: Graph which compares the relative cost of different benchmarks. All results related to use_error_code are fairly low (50 or lower), but the use_exception bars get dramatically higher (over 4000 for use_exception90) the greater the probability of throwing an exception is.
    Source: @Arash's answer containing this benchmark.

    In this benchmark, the cost of returning an error code is mostly the same no matter how great the probability is. Throwing an exception can be over 100x slower. It's expensive because

    • The stack must be unwound. This is effectively single-threaded and requires synchronization, see P2544: C++ exceptions are becoming more and more problematic.
    • Code is run which is probably not cached because exception handling code is never executed normally.
    • Exceptions are allocated in dynamic memory.

    If exceptions are so expensive, when do we use them?

    Exceptions are zero-overhead as long as you never throw them, but if you ever do, you're in for a wild ride. That's why, as the name says, they are used for exceptional situations, like:

    • running out of memory on an allocation
    • getting an unexpected error from a driver or the operating system
    • developer mistakes like accessing the wrong index on std::vector::at

    In a regular program execution, an exception should never be thrown.

    Alternative ways to return one of N types

    This topic has already been discussed to death in the threads you've linked, but just to give you a few options:

    • std::variant (C++17)
    • std::optional (C++17) and std::expected (C++23) in specific cases
    • polymorphic classes
      • possibly wrapped in std::unique_ptr (C++11)
    • struct which simply contains both types, and a tag to say which is active
      • possibly less efficient than a proper union, but trivial to implement
    • avoiding runtime polymorphism entirely with static polymorphism
      • possibly through templates

    Your example is a bit too minimal to say which way works best here.