First, some motivating background info; I'm experimenting with the idea of representing error-codes (returned from functions) as super-lightweight human-readable-strings rather than enum-integers, something like this:
#include <string.h>
/** Ultra-lightweight type for returning the success/error status from a function. */
class my_result_t
{
public:
/** Default constructor, creates a my_result_t indicating success */
my_result_t() : _errorString(NULL) {/* empty */}
/** Constructor for returning an error-result */
my_result_t(const char * s) : _errorString(s) {/* empty */}
/** Returns true iff the result was "success" */
bool IsOK() const {return (_errorString == NULL);}
/** Returns true iff the result was an error of some type */
bool IsError() const {return (_errorString != NULL);}
/** Return a human-readable description of the result */
const char * GetDescription() const {return _errorString ? _errorString : "Success";}
/** Returns true iff the two objects are equivalent */
bool operator ==(const my_result_t & rhs) const
{
return _errorString ? ((rhs._errorString)&&(strcmp(_errorString, rhs._errorString) == 0)) : (rhs._errorString == NULL);
}
/** Returns true iff the two objects are not equivalent */
bool operator !=(const my_result_t & rhs) const {return !(*this==rhs);}
private:
const char * _errorString;
};
my_result_t SomeFunction()
{
FILE * fp = fopen("some_file.txt", "r");
if (fp)
{
fclose(fp);
return my_result_t(); // success!
}
else return my_result_t("File not Found");
}
int main(int, char **)
{
printf("SomeFunction returned [%s]\n", SomeFunction().GetDescription());
return 0;
}
... the idea being that instead of having to maintain a centralized registry of "official" error codes somewhere, any function could simply return a string describing their particular error-condition in a human-readable fashion. Since sizeof(my_result_t)==sizeof(const char *)
, this shouldn't be significantly less efficient than the traditional return-an-integer-error-code convention that e.g. POSIX likes to use. (I do have to be careful not to return pointers-to-temporary-char-buffers, of course)
... all that works reasonably well; my question concerns the subsequent refinement, which is the creation of some global my_result_t
definitions for certain common error-types, e.g.:
const my_result_t RESULT_OUT_OF_MEMORY("Out of Memory");
const my_result_t RESULT_ACCESS_DENIED("Access Denied");
const my_result_t RESULT_BAD_ARGUMENT("Bad Argument");
const my_result_t RESULT_FILE_NOT_FOUND("File not Found");
[...]
... that way the author of SomeFunction()
could just return RESULT_FILE_NOT_FOUND;
rather than being required to enter a custom error-string and risk typos, inconsistency with other function's result-strings for the same type of error, etc.
My question is, what is the most runtime-efficient way to declare these common/global result-codes?
One way to do it would be to make them 'singleton objects', like this:
// my_result_t.h
extern const my_result_t RESULT_OUT_OF_MEMORY("Out of Memory");
extern const my_result_t RESULT_ACCESS_DENIED("Access Denied");
extern const my_result_t RESULT_BAD_ARGUMENT("Bad Argument");
extern const my_result_t RESULT_FILE_NOT_FOUND("File not Found");
// my_result_t.cpp
const my_result_t RESULT_OUT_OF_MEMORY("Out of Memory");
const my_result_t RESULT_ACCESS_DENIED("Access Denied");
const my_result_t RESULT_BAD_ARGUMENT("Bad Argument");
const my_result_t RESULT_FILE_NOT_FOUND("File not Found");
... or the other way, is to simply put the following in a central header file:
// my_result_t.h
const my_result_t RESULT_OUT_OF_MEMORY("Out of Memory");
const my_result_t RESULT_ACCESS_DENIED("Access Denied");
const my_result_t RESULT_BAD_ARGUMENT("Bad Argument");
const my_result_t RESULT_FILE_NOT_FOUND("File not Found");
... this latter way seems to work fine, but I'm not 100% confident it's kosher to do that; for one thing, it means that e.g. RESULT_OUT_OF_MEMORY is a separate object in every translation unit, which seems like it might put stress on the linker to de-duplicate, or even invoke conceivably undefined behavior (I'm not sure how the One Definition Rule applies here). On the other hand, using the "extern" approach means that the actual contents of the my_result_t
objects are only available to optimizer when compiling my_result_t.cpp
, and not when compiling any other functions that reference these objects, which means the optimizer might not be able do inlining-optimizations.
Is one approach better than the other, from the perspective of correctness, but also helping the optimizer to make the code as efficient as possible?