How do I implement my exceptions in C ++?

Let's say I have a C ++ library. In some cases, the library will throw exceptions. I would like to make it possible and easy for the user of this library to catch these exceptions and tell what went wrong. What's the best way to handle this? I came up with the following possible solutions:

  • Just use the standard exception classes with custom error messages. This, however, would make it annoying for anyone catching an exception to say what is wrong.
  • Subclass std::exception

    and throw it away. Add an error code (perhaps an enum? Or a macro?) So the user can check what went wrong.
  • Multiple subclassing of exceptions: one for each possible case an exception can be thrown. This sounds like a neat idea, but I find it too overwhelming to subclass for every possible error.

I cannot decide. What's the correct way?

+3


source to share


2 answers


Exceptions thrown due to incorrect program writing should be obtained from std::logic_error

. Out-of-bounds indices are examples of this. A std::logic_error

is the error you would expect and most programs will not be able to repair them.

Exceptions that are recoverable (i.e. something cannot be done due to resource unavailability) should be obtained from std::runtime_error

.

the exception type should explain what went wrong. Any supporting information that might be of interest to the developer can be on the line what()

. If you want to deliver a chain of causality to a developer, consider using std::throw_with_nested

. This allows the interested developer to get a legal basis to find out why their operation failed without having to use your source code.

Consider using the exception hierarchy. This allows the consumer of your class to easily code the overall failure state, allowing it to check for individual failures if they are important.

contrived example:



struct failed_to_start : std::runtime_error { 
  using std::runtime_error::runtime_error; 
};

struct engine_fault : failed_to_start { 
  using std::failed_to_start::failed_to_start; 
};

struct engine_flooded : engine_fault { 
  using std::engine_fault::engine_fault; 
};

struct wrong_key : std::logic_error {
  using std::logic_error::logic_error;
};

      

EDIT: As requested, a complete working example of using throw_with_nested (and a few other useful methods)

#include <iostream>
#include <string>
#include <stdexcept>


enum class key {
    none, mine, yours
};

struct out_of_fuel : std::runtime_error {
    using std::runtime_error::runtime_error;
};

struct no_key : std::runtime_error {
    using std::runtime_error::runtime_error;
};

struct start_failure : std::runtime_error {
    using std::runtime_error::runtime_error;
};

struct wrong_key_error : std::logic_error {
    using std::logic_error::logic_error;
};

struct car_configuration_error : std::logic_error {
    using std::logic_error::logic_error;
};

struct fuel_tank {
    fuel_tank(double initial) : _quantity { initial } {}

    void remove_fuel(double amount) {
        using namespace std;
        if (amount > _quantity) {
            throw out_of_fuel { "fuel tank has "s
                + to_string(_quantity)
                + " litres remaining, tried to remove "s
                + to_string(amount) };
        }
        _quantity -= amount;
    }

    double _quantity = 0.0;
};

struct ignition {
    ignition(key k) : _key_type { k } {}

    void insert_key(key k) {
        if (_key_type != k) {
            throw wrong_key_error { "the wrong key was inserted" };
        }
        _current_key = k;
    }

    void turn_key() {
        if (_current_key != _key_type) {
            throw no_key { "there is no key in the ignition" };
        }
    }

    key _current_key = key::none;
    const key _key_type;
};

struct engine {
    void run() {

    }

};

struct car {
    car(key k, double initial_fuel)
    : _ignition(k)
    , _fuel_tank(initial_fuel)
    {}

    void start(key k)
    try
    {
        _ignition.insert_key(k);
        _ignition.turn_key();
        _fuel_tank.remove_fuel(1);
        _engine.run();
    }
    catch(const std::logic_error& e) {
        std::throw_with_nested(car_configuration_error { "car configuration error - please check your program" });
    }
    catch(const std::exception& e) {
        std::throw_with_nested(start_failure { "failed to start car" });
    }

    ignition _ignition;
    engine _engine;
    fuel_tank _fuel_tank;
};

void print_current_exception(int level = 0);

void print_exception(const std::exception&e, const char* prefix, int level)
{
    std::cerr << std::string(level, ' ') << prefix << ": " << e.what() << '\n';
    try {
        std::rethrow_if_nested(e);
    }
    catch(const std::exception&) {
        print_current_exception(level + 1);
    }
}

void print_current_exception(int level)
{
    auto eptr = std::current_exception();
    if (!eptr)
        return;

    try {
        std::rethrow_exception(eptr);
    }
    catch(const std::logic_error& e) {
        print_exception(e, "logic error", level);
    }
    catch(const std::runtime_error& e) {
        print_exception(e, "runtime error", level);
    }
    catch(const std::exception& e) {
        print_exception(e, "exception", level);
    }
}

int main(int argc, const char * argv[])
{
    car my_car { key::mine, .05 };
    car your_car { key::yours, 100 };

    try {
        my_car.start(key::mine);
    }
    catch(const std::exception&) {
        print_current_exception();
    }

    try {
        your_car.start(key::mine);
    }
    catch(const std::exception&) {
        print_current_exception();
    }
    return 0;
}

      

expected output:

runtime error: failed to start car
 runtime error: fuel tank has 0.050000 litres remaining, tried to remove 1.000000
logic error: car configuration error - please check your program
 logic error: the wrong key was inserted

      

+2


source


3. Create multiple exception subclasses: one for each possible case can be thrown an exception. This sounds like a neat idea, but I find it too overwhelming to subclass for every possible error.

This is for the reason you stated: your users can then catch exactly what they want to catch.

In short, use the exceptions that should have been used.



2. Create a subclass std::exception

and discard it. Add some sort of error code (maybe an enum? Or a macro?) So the user can check what went wrong.

You might want to skimp on this approach a little when one semantic type of exception has "subcategories" or other ancillary data that you never want to filter, but might be secondary to your users.

+6


source







All Articles