I need to write C++ API, which consists of several exported C++ classes exposed from Dll, using .lib files (MSVC). From the answer to my another question I understand, that exported class methods cannot use exceptions, in the case if C++ API is built in one VC++ version (let's say 2010), and client code is written in another VC++ version. Since exceptions cannot be a part of public API interface, I am looking for another error handling strategy. My restrictions: I don't want to use COM, and rich error code system (like HRESULT) is not enough for me. I want to have exception-like class which contains error code, error message and any other information that I need. Also, I don't want to make separate build for every VC++ version.
My current approach is the following. Every public class method returns enumerated value (like ErrorCode). In the case the method fails, static function like GetLastErrorInfo returns the pointer to C++ class (let's say ErrorInfo), which contains reach error information. ErrorInfo is kept as thread-specific data, and contains error information from the last call in the current thread. If last API call succeeded, GetErrorInfo returns NULL.
Consider this code with exceptions:
try
{
classPtr->DoSomething();
cout << classPtr->GetData() << endl;
}
catch(const MyException& ex)
{
cout << ex.GetErrorMessage() << endl;
return;
}
Without exceptions, it looks like this:
ErrorCode result;
int data;
result = classPtr->DoSomething();
if ( result != Success )
{
cout << MyClass::GetLastErrorInfo()->GetErrorMessage() << endl;
return;
}
result = classPtr->GetData(data);
if ( result != Success )
{
cout << MyClass::GetLastErrorInfo()->GetErrorMessage() << endl;
return;
}
cout << data << endl;
This doesn't look good. Class interface is messy: every function now has ErrorCode return type. Return values become output parameters. Is there better approach, that allows to have reach error information, and to keep clean public API interface?
You are possibly overlooking a simple solution. The only restriction is that an exception cannot cross a module boundary. There is no problem with the client code itself throwing an exception. So provide an inline function in the header, say CheckReturn(), that throws the rich exception.
For inspiration, look at the COM IErrorInfo interface and its associated _com_error class. They solve the exact same problem. Also note the #import directive available in MSVC, it auto-generates the small wrapper functions that make the call and throw the exception on a failure return value. But you don't want to use COM so that's not directly usable.
You have to extremely careful if you return C++ objects from a .dll because your caller may try to use those objects in ways that involve making copies of them or deleting them. If the caller is not using the same heap (or the same standard library), you'll have all kinds of crashes and memory leaks all over the place. In my experience, passing C++ objects over DLL boundaries is a bad idea.
I'd try to create the API in a way to require the caller to manage all memory (allocation and deallocation) so that ownership of pointers is always clear. This creates more work since you need to accept pointers and then fill those buffers with data instead of returning C++ objects. I haven't seen a working implementation of C++ objects safely being passed among .dlls. (But then my experience may be limited.)
Also, when you export C++ classes from a .dll, it's unlikely that anything other than C++ can use that .dll, since other languages have different memory layouts for objects. Even different C++ compilers may have problems using those classes.
Re-reading your constraints, I'd say creating COM objects is one of your best options since it gives you flexibility and the ability to return objects with complex data (although not C++ objects).
How about this:
Basic error data - you may extend for your lib's "other stuff":
namespace MON {
class t_error_description {
public:
t_error_description(const int& code, const std::string& message);
virtual ~t_error_description(); /* << allow any other info via subclass */
public:
virtual void description(std::ostream& stream) const;
/* … */
private:
const int d_code;
const std::string d_message;
};
}
Basic error container. Wraps everything relative to the description, and this is all the client deals with directly:
namespace MON {
class t_error {
public:
t_error();
~t_error();
public:
/* or perhaps you'd favor a stream op? */
void description(std::ostream&) const;
/* sets the error - this is a take operation */
void set(const t_error_description* const desc);
void clear();
/* … */
private:
/* trivial construction */
t_auto_pointer<const t_error_description> d_errorDescription;
private:
/* verboten */
t_error(const t_error&);
t_error& operator=(const t_error&);
};
}
Basic lib call:
namespace MON {
/* return false on error */
bool DoSomething(t_error& outError) {
if (Foo()) {
outError.set(new t_error_description(ErrorCodeThingy, "blah blah"));
return false;
}
return true;
}
}
Client call:
MON::t_error err;
if (!MON::DoSomething(err)) {
log << "cannot do anything!\nError: ";
err.description(log);
return;
}