Polymorphic exception handling: How to catch subclass exception?
Asked Answered
K

2

24

I have the following simple hierarchy of two C++ exceptions:

class LIB_EXP ClusterException : public std::exception {
public:
    ClusterException() { }
    ClusterException(const std::string& what) { init(what); }
    virtual const char* what() const throw() { return what_.c_str(); }
    virtual ~ClusterException() throw() {}
    virtual ClusterException* clone() { return new ClusterException(*this);  } 
protected:
    void init(const std::string& what) { what_ = what; }
private:
    std::string what_;
};

class LIB_EXP ClusterExecutionException : public ClusterException {
public:
    ClusterExecutionException(const std::string& jsonResponse);
    std::string getErrorType() const throw() { return errorType_; }
    std::string getClusterResponse() const throw() { return clusterResponse_; }
    virtual ~ClusterExecutionException() throw() {}
    virtual ClusterExecutionException* clone() { return new ClusterExecutionException(*this);  } 
private:
    std::string errorType_;
    std::string clusterResponse_;
};

I then export them to Python with Boost-Python as follows. Note my use of the bases to make sure that the inheritance relationship is preserved in the translation:

class_<ClusterException> clusterException("ClusterException", no_init);
clusterException.add_property("message", &ClusterException::what);
clusterExceptionType = clusterException.ptr();
register_exception_translator<ClusterException>(&translateClusterException);

class_<ClusterExecutionException, bases<ClusterException> > clusterExecutionException("ClusterExecutionException", no_init);
clusterExecutionException.add_property("message", &ClusterExecutionException::what)
                         .add_property("errorType", &ClusterExecutionException::getErrorType)
                         .add_property("clusterResponse", &ClusterExecutionException::getClusterResponse);
clusterExecutionExceptionType = clusterExecutionException.ptr();
register_exception_translator<ClusterExecutionException>(&translateClusterExecutionException);

Then the exception translation method:

static PyObject *clusterExceptionType = NULL;
static void translateClusterException(ClusterException const &exception) {
  assert(clusterExceptionType != NULL); 
  boost::python::object pythonExceptionInstance(exception);
  PyErr_SetObject(clusterExceptionType, pythonExceptionInstance.ptr());
}

static PyObject *clusterExecutionExceptionType = NULL;
static void translateClusterExecutionException(ClusterExecutionException const &exception) {
  assert(clusterExecutionExceptionType != NULL);
  boost::python::object pythonExceptionInstance(exception);
  PyErr_SetObject(clusterExecutionExceptionType, pythonExceptionInstance.ptr());
}

I created the following test C++ function that throws the exceptions:

static void boomTest(int exCase) {
  switch (exCase) {
    case 0:  throw ClusterException("Connection to server failed");
             break;
    case 1:  throw ClusterExecutionException("Error X while executing in the cluster");
             break;
    default: throw std::runtime_error("Unknown exception type");
  }
}

Finally the Python test code that calls my C++ boomTest:

import cluster
reload(cluster)
from cluster import ClusterException, ClusterExecutionException

def test_exception(exCase):
    try:
        cluster.boomTest(exCase)

    except ClusterException as ex:
        print 'Success! ClusterException gracefully handled:' \
            '\n message="%s"' % ex.message
    except ClusterExecutionException as ex:
        print 'Success! ClusterExecutionException gracefully handled:' \
            '\n message="%s"' \
            '\n errorType="%s"' \
            '\n clusterResponse="%s"' % (ex.message, ex.errorType, ex.clusterResponse)
    except:
        print 'Caught unknown exception: %s "%s"' % (sys.exc_info()[0], sys.exc_info()[1])

def main():
    print '\n************************ throwing ClusterException ***********************************************************************'
    test_exception(0)
    print '\n************************ throwing ClusterExecutionException **************************************************************'
    test_exception(1)
    print '\n************************ throwing std::runtime_error *********************************************************************'
    test_exception(2)

if __name__ == "__main__":
    main()

Up to here it all works. However if I remove the ClusterExecutionException catch handler from Python then this exception will be caught and fallback to an unknown exception instead of being caught as its base ClusterException.

I have tried in Boost-Python while registering the exception translation of ClusterExecutionException to register it as its base ClusterException and then it gets caught "polymorphically" but then it won't be caught as a ClusterExecutionException. How can make it so that ClusterExecutionException gets caught as both ClusterException and ClusterExecutionException? I have tried of course registering this ClusterExecutionException exception as both ClusterException and ClusterExecutionException but it follows a last wins strategy, and only one works not both.

Is there any other way to solve this?

UPDATE 1: The wholy grail of this problem is to find out on the C++ side the type of the except Python statement e.g. except ClusterException as ex: which is unknown once inside the C++ side. The exception translate by Boost.Python will call the translate function that corresponds to the dynamic type of the exception and the Python catch static type is not known.

UPDATE 2: As suggested changing the Python code to the following i.e. adding print(type(ex).__bases__) gives:

def test_exception(exCase):
    try:
        cluster.boomTest(exCase)

    except ClusterException as ex:
        print(type(ex).__bases__)
        print 'Success! ClusterException gracefully handled:' \
            '\n message="%s"' % ex.message
    except ClusterExecutionException as ex:
        print(type(ex).__bases__)
        print 'Success! ClusterExecutionException gracefully handled:' \
            '\n message="%s"' \
            '\n errorType="%s"' \
            '\n clusterResponse="%s"' % (ex.message, ex.errorType, ex.clusterResponse)
    except:
        print 'Caught unknown exception: %s "%s"' % (sys.exc_info()[0], sys.exc_info()[1])

and the output:

************************ throwing ClusterException ***********************************************************************
(<type 'Boost.Python.instance'>,)
Success! ClusterException gracefully handled:
 message="Connection to server failed"

************************ throwing ClusterExecutionException **************************************************************
(<class 'cluster.ClusterException'>,)
Success! ClusterExecutionException gracefully handled:
 message="Error X while executing in the cluster"
 errorType="LifeCycleException"
 clusterResponse="{ "resultStatus": "Error", "errorType": "LifeCycleException", "errorMessage": "Error X while executing in the cluster" }"

Meaning that the inheritance relationship is "seen" from Python. But the polymorphic handling still doesn't work.

UPDATE 3 This is the output of running VS dumpbin.exe:

The command I used is:

dumpbin.exe /EXPORTS /SYMBOLS C:\ClusterDK\x64\Debug\ClusterDK.dll > c:\temp\dumpbin.out

and the relevant parts of the output:

Microsoft (R) COFF/PE Dumper Version 11.00.50727.1
Copyright (C) Microsoft Corporation.  All rights reserved.

Dump of file C:\ClusterDK\x64\Debug\ClusterDK.dll

File Type: DLL

Section contains the following exports for ClusterDK.dll

00000000 characteristics
5A1689DA time date stamp Thu Nov 23 09:42:02 2017
    0.00 version
       1 ordinal base
      78 number of functions
      78 number of names

ordinal hint RVA      name

      8    7 00004485 ??0ClusterException@cluster@@QEAA@AEBV01@@Z = @ILT+13440(??0ClusterException@cluster@@QEAA@AEBV01@@Z)
      9    8 00001659 ??0ClusterException@cluster@@QEAA@AEBV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z = @ILT+1620(??0ClusterException@cluster@@QEAA@AEBV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z)
     10    9 00001F1E ??0ClusterException@cluster@@QEAA@XZ = @ILT+3865(??0ClusterException@cluster@@QEAA@XZ)
     11    A 00004D4F ??0ClusterExecutionException@cluster@@QEAA@AEBV01@@Z = @ILT+15690(??0ClusterExecutionException@cluster@@QEAA@AEBV01@@Z)
     12    B 000010AA ??0ClusterExecutionException@cluster@@QEAA@AEBV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z = @ILT+165(??0ClusterExecutionException@cluster@@QEAA@AEBV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z)
     27   1A 000035D0 ??1ClusterException@cluster@@UEAA@XZ = @ILT+9675(??1ClusterException@cluster@@UEAA@XZ)
     28   1B 00003C7E ??1ClusterExecutionException@cluster@@UEAA@XZ = @ILT+11385(??1ClusterExecutionException@cluster@@UEAA@XZ)
     37   24 00002BD5 ??4ClusterException@cluster@@QEAAAEAV01@AEBV01@@Z = @ILT+7120(??4ClusterException@cluster@@QEAAAEAV01@AEBV01@@Z)
     38   25 000034D1 ??4ClusterExecutionException@cluster@@QEAAAEAV01@AEBV01@@Z = @ILT+9420(??4ClusterExecutionException@cluster@@QEAAAEAV01@AEBV01@@Z)
     46   2D 000D2220 ??_7ClusterException@cluster@@6B@ = ??_7ClusterException@cluster@@6B@ (const cluster::ClusterException::`vftable')
     47   2E 000D2248 ??_7ClusterExecutionException@cluster@@6B@ = ??_7ClusterExecutionException@cluster@@6B@ (const cluster::ClusterExecutionException::`vftable')
     52   33 00004BB5 ?clone@ClusterException@cluster@@UEAAPEAV12@XZ = @ILT+15280(?clone@ClusterException@cluster@@UEAAPEAV12@XZ)
     53   34 00004D31 ?clone@ClusterExecutionException@cluster@@UEAAPEAV12@XZ = @ILT+15660(?clone@ClusterExecutionException@cluster@@UEAAPEAV12@XZ)
     61   3C 00001D43 ?getErrorType@ClusterExecutionException@cluster@@QEBA?AV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@XZ = @ILT+3390(?getErrorType@ClusterExecutionException@cluster@@QEBA?AV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@XZ)
     69   44 0000480E ?init@ClusterException@cluster@@IEAAXAEBV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z = @ILT+14345(?init@ClusterException@cluster@@IEAAXAEBV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z)
     78   4D 000032FB ?what@ClusterException@cluster@@UEBAPEBDXZ = @ILT+8950(?what@ClusterException@cluster@@UEBAPEBDXZ)

Summary

    4000 .data
    5000 .idata
   12000 .pdata
   54000 .rdata
    2000 .reloc
    1000 .rsrc
   C9000 .text
    1000 .tls
Kandi answered 26/9, 2017 at 7:41 Comment(12)
When registering both, did you also overload the translateClusterExecutionException(..) method for the base class type? You may need to dynamic_cast<>() the &ref into the derived pointer type to get the correct behavior again.Malherbe
Hi, can you show a demonstration of what you mean ... preferably as an answer with as much detail as you can and I will test it ...Kandi
When you remove the lines except ClusterExecutionException as ex: ... , could you verify the base class of the exception you get? e.g. except Exception as e: print(type(e).__bases__)Medford
@JuanjoMartin Thank you! done created update2 in the OP it shows the base class correcly.Kandi
Since the base class of ClusterExecutionException is cluster.ClusterException, have you tried catching the exception except cluster.ClusterException as ex:?Medford
Good I did just now to try except cluster.ClusterException as ex:, unfortunately it is the same same if the translate from C++ maps to the dynamic type of the derived ClusterExecutionException then it won't be caught as ClusterExceptionKandi
From your answer I'm not sure if cluster.ClusterException is caught. If the answer is yes, you could catch both exceptions in the same clause with except (ClusterException, cluster.ClusterException) as e:Medford
It is not caught. Just to be totally clear, when I throw a cluster.ClusterExecutionException then the Python cluster.ClusterException handler will not catch it. The only way it is caught is when I explicitly translate on Boost.Python C++ side a cluster.ClusterExecutionException to a cluster.ClusterException but then it won't be caught as cluster.ClusterExecutionExceptionKandi
Would this be a Boost.Python bug?Kandi
Maybe my question is not related - but is the definition of clone() required by the Boost translation mechanism, or have you added it for other reasons?Westing
@Westing good point. clone() is implemented not because of the Boost translation. I do not think that Boost cares about or uses it. It is introduced because of the architecture of the Cluster client implementation. Exceptions are raised and caught in a thread different than the one that interfaces with Python and therefore the cloning to be able to propagate it to a different thread without it going out of scope.Kandi
Okay. I thought that ptr() uses clone() but if is not related to Boost, this is not the case...Westing
M
1

My python skills are rusty and I haven't tested this, so this may need further improvements, but try adding the exception translation method to handle the base class exception type:

static PyObject *clusterExecutionAsClusterExceptionType = NULL;
static void translateClusterExecutionAsClusterException(ClusterException const &exception) {

  ClusterExecutionException* upcasted = dynamic_cast<ClusterExecutionException*>(&exception);
  if (upcasted)
  {
    assert(clusterExecutionAsClusterExceptionType != NULL);
    boost::python::object pythonExceptionInstance(*upcasted);
  PyErr_SetObject(clusterExecutionAsClusterExceptionType, pythonExceptionInstance.ptr());
  }
}

register_exception_translator<ClusterException>(&translateClusterExecutionAsClusterException);
Malherbe answered 4/10, 2017 at 10:3 Comment(3)
Thank you, I have tried what you posted but it doesn't work. I have also tried multiple derivations of it but neither work. The holy grail of this problem is the exception type mapping and making it match the type of the Python exception in the except (catch) statement that is unknown to C++ ...Kandi
It's been a while but I just thought of something.. If you are able to dump all exported symbols (dumpbin.exe) from the python dll that contains the function/object signatures, you may be able to piece together the signature that you need to trigger the exception.Malherbe
Good point! I updated the question. If we find a solution I will be happy to accept your answer and request a transfer of the 100 points to you.Kandi
F
0

I don't know about your C++ code. But there is small problem with your python code. Catch ClusterExecutionException before ClusterException. You should always put child exception handler before base exception.

In test_exception if ClusterExecutionException raised it will be catch by ClusterException before it could reach ClusterExecutionException.

code should be as below code

def test_exception(exCase):
    try:
        cluster.boomTest(exCase)

    except ClusterExecutionException as ex:
        print 'Success! ClusterExecutionException gracefully handled:' \
            '\n message="%s"' \
            '\n errorType="%s"' \
            '\n clusterResponse="%s"' % (ex.message, ex.errorType, ex.clusterResponse)
    except ClusterException as ex:
        print 'Success! ClusterException gracefully handled:' \
            '\n message="%s"' % ex.message
    except:
        print 'Caught unknown exception: %s "%s"' % (sys.exc_info()[0], sys.exc_info()[1])

now do I have tried in Boost-Python while registering the exception translation of ClusterExecutionException to register it as its base ClusterException , what you have mentioned in question.

False answered 6/10, 2017 at 9:28 Comment(4)
Thank you but this AFAIK belong to a comment and not an answer. The order in the exception handling is not the usual one, and this also illustrates the problem I described in the OP. If the issue was what you answered I would be always handling ClusterException because it would fall to the base class polymorphically but it doesn't work and hence the OP.Kandi
I put it on answer because i didn't had privilege to comment.False
Ah ok got ya :)Kandi
This answer plus getting the 100 pts is totally misleading ... this anwer only points out a best practice I know without solving the OP. People visiting this question will waste time mislead by thinking that the solution to the OP was simply to reorder the exception handling. The other answer is more deserving of the 100 pts and to be at the top.Kandi

© 2022 - 2024 — McMap. All rights reserved.