C++11 Mapping for Exceptions

On this page:

Base Class for Ice Exceptions

The class Ice::Exception is the root of the derivation tree for Ice exceptions, and encapsulates functionality that is common to all Ice exceptions:

C++
class Exception : public std::exception
{
public:
 
    Exception();
    Exception(const char* file, int line);

    virtual std::string ice_id() const = 0;
    virtual void ice_print(std::ostream&) const;
    virtual const char* what() const noexcept override;
    virtual void ice_throw() const = 0;
 
    std::unique_ptr<Exception> ice_clone() const;
        
    const char* ice_file() const;
    int ice_line() const;
    std::string ice_stackTrace() const;
 
protected:
 
    virtual Exception* ice_cloneImpl() const = 0;
};

The second constructor stores a file name and line number in the exception that are returned by the ice_file and ice_line member functions, respectively. This allows you to identify the source of an exception by passing the __FILE__ and __LINE__ preprocessor macros to the constructor.

Each exception has the following member functions:

  • ice_clone
    This member function allows you to polymorphically clone an exception. For example:

    C++
    try 
    {
        // ...
    }
    catch(const Ice::UserException& e)
    {
       auto copy = e.clone();
    } 
    

    ice_clone is useful if you need to make a copy of an exception without knowing its precise run-time type. This allows you to remember the exception and throw it later by calling ice_throw. ice_clone is implemented through the virtual function ice_cloneImpl.

  • ice_id
    As the name suggests, this member function returns the type ID of the exception. For example, if you call the ice_id member function of a BadZoneName exception defined in module M, it returns the string "::M::BadZoneName". The ice_id member function is useful if you catch exceptions generically and want to produce a more meaningful diagnostic, for example:

    C++
    try
    {
        // ...
    } 
    catch(const GenericError& e)
    {
        cerr << "Caught an exception: " << e.ice_id() << endl;
    }
    

    If an exception is raised, this code prints the id of the actual exception (such as ::M::BadTimeVal or ::M::BadZoneName). For exception that are not defined in Slice, ice_id returns the full name of the C++ class.

  • ice_file
    ice_file
    returns the file name provided to the second constructor of Exception.

  • ice_line
    ice_line
     returns the line number provided to the second constructor of Exception.

  • ice_print
    The default implementation of ice_print prints the file name and line number (when available), and the type ID of the exception.

  • ice_stackTrace
    The ice_stackTrace function returns the full stack trace when the exception was constructed, or an empty string, depending on the value of the Ice.PrintStackTraces property.

  • ice_throw
    ice_throw allows you to throw an exception without knowing its precise run-time type. It is implemented as:

    C++
    void
    GenericError::ice_throw() const
    {
        throw *this;
    }
    

    You can call ice_throw to throw an exception that you previously cloned with ice_clone.

  • what
    The default implementation of what returns a string created by ice_print.

C++ Mapping for User Exceptions

Here is a fragment of the Slice definition for our world time server once more:

Slice
exception GenericError
{
    string reason;
}
exception BadTimeVal extends GenericError {}
exception BadZoneName extends GenericError {}

These exception definitions map as follows:

C++
class GenericError: public Ice::UserException
{
public:
    
    GenericError() = default;
    explicit GenericError(const std::string&);
    
    virtual std::string ice_id() const override;
    virtual void ice_throw() const override;
    std::unique_ptr<GenericError> ice_clone() const;
 
    std::tuple<const std::string&> ice_tuple() const;
   
    static const std::string& ice_staticId();
 
    std::string reason;
 
protected:
    
   virtual Exception* ice_cloneImpl() const override;
};

class BadTimeVal: public GenericError
{
public:
 
    BadTimeVal() = default;
    explicit BadTimeVal(const std::string&);

    virtual std::string ice_id() const override;
    virtual void ice_throw() const override;
    std::unique_ptr<BadTimeVal> ice_clone() const;
   
    static const std::string& ice_staticId();
 
protected:
    
   virtual Exception* ice_cloneImpl() const override;
};

class BadZoneName: public GenericError
{
public:
 
    BadZoneName() = default;
    explicit BadZoneName(const std::string&);
 
    virtual std::string ice_id() const override;
    virtual void ice_throw() const override;
    std::unique_ptr<BadZoneName> ice_clone() const;
   
    static const std::string& ice_staticId();
 
protected:
    
   virtual Exception* ice_cloneImpl() const override;
};

Each Slice exception is mapped to a C++ class with the same name. For each exception member, the corresponding class contains a public data member. (Since BadTimeVal and BadZoneName do not have members, the generated classes for these exceptions also do not have members.)

The inheritance structure of the Slice exceptions is preserved for the generated classes, so BadTimeVal and BadZoneName inherit from GenericError.

Each exception has a default constructor. Members having a complex type, such as strings, sequences, and dictionaries, are initialized by their own default constructor. However, the default constructor performs no initialization for members having one of the simple built-in types boolean, integer, floating point, or enumeration. For such a member, it is not safe to assume that the member has a reasonable default value. This is especially true for enumerated types as the member's default value may be outside the legal range for the enumeration, in which case an exception will occur during marshaling unless the member is explicitly set to a legal value.

To ensure that data members of primitive types are initialized to reasonable values, you can declare default values in your Slice definition. The default constructor initializes each of these data members to its declared value. Optional data members are unset unless they declare default values.

An exception also has a second constructor that accepts one argument for each exception member. This constructor allows you to instantiate and initialize an exception in a single statement, instead of having to first instantiate the exception and then assign to its members. For each optional data member, its corresponding constructor parameter uses the same mapping as for operation parameters.

For derived exceptions, the constructor accepts one argument for each base exception member, plus one argument for each derived exception member, in base-to-derived order.

All user exceptions provide an ice_staticId static member function that returns the type-id of the exception. For example BadZoneName::ice_staticId() returns "::M::BadZoneName".

All user exceptions ultimately inherit from Ice::UserException. In turn, Ice::UserException inherits from Ice::Exception:

C++
namespace Ice
{
    class Exception : public std::exception
    {
        // ...
    };
    std::ostream& operator<<(std::ostream&, const Exception&);

    class UserException : public Exception
    {
        // ...
    };
}

To make printing more convenient, operator<< is overloaded for Ice::Exception, so you can also write:

C++
try
{
    // ...
}
catch(const Ice::Exception& e)
{
    cerr << e << endl;
}

This produces the same output because operator<< calls ice_print internally. You can optionally provide your own ice_print implementation using the cpp:ice_print metadata directive.

For Ice run time exceptions, ice_print also shows the file name and line number at which the exception was thrown.

C++ Mapping for Run-Time Exceptions

The Ice run time throws run-time exceptions for a number of pre-defined error conditions. All run-time exceptions directly or indirectly derive from Ice::LocalException (which, in turn, derives from Ice::Exception). Ice::LocalException has the usual member functions: ice_clone, ice_id, ice_throw, etc. They also provide an ice_staticId static member function like user exceptions.

Recall the inheritance diagram for user and run-time exceptions. By catching exceptions at the appropriate point in the hierarchy, you can handle exceptions according to the category of error they indicate:

  • Ice::Exception
    This is the root of the complete inheritance tree. Catching Ice::Exception catches both user and run-time exceptions. As shown earlier, Ice::Exception inherits from std::exception.

  • Ice::UserException
    This is the root exception for all user exceptions. Catching Ice::UserException catches all user exceptions (but not run-time exceptions).
  • Ice::LocalException
    This is the root exception for all run-time exceptions. Catching Ice::LocalException catches all run-time exceptions (but not user exceptions).
  • Ice::TimeoutException
    This is the base exception for both operation-invocation and connection-establishment timeouts.
  • Ice::ConnectTimeoutException
    This exception is raised when the initial attempt to establish a connection to a server times out.

For example, a ConnectTimeoutException can be handled as ConnectTimeoutException, TimeoutException, LocalException, or Exception.

You will probably have little need to catch run-time exceptions as their most-derived type and instead catch them as LocalException; the fine-grained error handling offered by the remainder of the hierarchy is of interest mainly in the implementation of the Ice run time. Exceptions to this rule are the exceptions related to facet and object life cycles, which you may want to catch explicitly. These exceptions are FacetNotExistException and ObjectNotExistException, respectively.

See Also