Reputation: 61
I cann't find a way to return an error from a function inside an function, and ending the program.
I was thinking of using a struct and storing the returning value inside and end it this way, but I don't know if this is the best practise
Lets say I have a programm like this:
int main()
{
// do stuff
importantFunction();
// do stuff
return 0;
}
In the importantFunction() I am calling two other functions which are doing some Bitshifting and are returning an array always. Now I want to try to return 1 (or 0x01 because the function is returning an array pointer) if an error happends in one of this function, but I am not sure how.
char *importantFunction()
{
//do stuff
secoundFunction();
thirdFunction();
//do stuff
return array;
}
char *secoundFunction()
{
// do stuff
if (something == x)
return array;
// do stuff
return array;
}
I am just trying to find an methode without having to check the first function if it is equall to something and then end the program in int main. I am trying to avoid this, because it is not working always:
int main()
{
// do stuff
char *pointer = importantFunction();
if (*pointer == 'something')
return 1;
if (*pointer == 'something')
return 2;
if (*pointer == 'something')
return 3;
// and so on...
// do stuff
return 0;
}
I am sorry if this is a stupid question, I am not really good in asking questions.
Upvotes: 1
Views: 401
Reputation: 70909
C has out-of-band error code handling, it's been in there since forever.
#include <errno.h>
int do_something(char* data) {
if ( data == 0 ) {
errno = ENODATA;
return 0;
}
... do stuff ...
}
... in the caller ...
int value = do_something( "one" );
if ( int errornum = errno ) {
fprintf("error (%d) could not do something: %s", strerror( errornum ) );
return; // or exit;
}
if wishing to chain errors
int value = do_something( "one" );
if ( int errornum = errno ) {
fprintf("error (%d) could not do something: %s", strerror( errornum ) );
errno = errornum;
return; // or exit;
}
Keep in mind that errno is reset by nearly every standard function call, so you need to capture it and then optionally set it again after you do whatever you want.
The reason errno is typically not used as heavily as it should be is probably becasue too many people are taught in-band error reporting (through special sentinels / values) first. In addition, it takes more lines of code to properly check error codes. That said, it is a far better solution, as you don't overload a return value with data and control information in the same variable.
There are already many error codes set, odd are you can reuse one for your needs, or pick one that's close enough
1 EPERM Operation not permitted
2 ENOENT No such file or directory
3 ESRCH No such process
4 EINTR Interrupted system call
5 EIO I/O error
6 ENXIO No such device or address
7 E2BIG Argument list too long
8 ENOEXEC Exec format error
9 EBADF Bad file number
10 ECHILD No child processes
11 EAGAIN Try again
12 ENOMEM Out of memory
13 EACCES Permission denied
14 EFAULT Bad address
15 ENOTBLK Block device required
16 EBUSY Device or resource busy
17 EEXIST File exists
18 EXDEV Cross-device link
19 ENODEV No such device
20 ENOTDIR Not a directory
21 EISDIR Is a directory
22 EINVAL Invalid argument
23 ENFILE File table overflow
24 EMFILE Too many open files
25 ENOTTY Not a typewriter
26 ETXTBSY Text file busy
27 EFBIG File too large
28 ENOSPC No space left on device
29 ESPIPE Illegal seek
30 EROFS Read-only file system
31 EMLINK Too many links
32 EPIPE Broken pipe
33 EDOM Math argument out of domain of func
34 ERANGE Math result not representable
35 EDEADLK Resource deadlock would occur
36 ENAMETOOLONG File name too long
37 ENOLCK No record locks available
38 ENOSYS Function not implemented
39 ENOTEMPTY Directory not empty
40 ELOOP Too many symbolic links encountered
42 ENOMSG No message of desired type
43 EIDRM Identifier removed
44 ECHRNG Channel number out of range
45 EL2NSYNC Level 2 not synchronized
46 EL3HLT Level 3 halted
47 EL3RST Level 3 reset
48 ELNRNG Link number out of range
49 EUNATCH Protocol driver not attached
50 ENOCSI No CSI structure available
51 EL2HLT Level 2 halted
52 EBADE Invalid exchange
53 EBADR Invalid request descriptor
54 EXFULL Exchange full
55 ENOANO No anode
56 EBADRQC Invalid request code
57 EBADSLT Invalid slot
59 EBFONT Bad font file format
60 ENOSTR Device not a stream
61 ENODATA No data available
62 ETIME Timer expired
63 ENOSR Out of streams resources
64 ENONET Machine is not on the network
65 ENOPKG Package not installed
66 EREMOTE Object is remote
67 ENOLINK Link has been severed
68 EADV Advertise error
69 ESRMNT Srmount error
70 ECOMM Communication error on send
71 EPROTO Protocol error
72 EMULTIHOP Multihop attempted
73 EDOTDOT RFS specific error
74 EBADMSG Not a data message
75 EOVERFLOW Value too large for defined data type
76 ENOTUNIQ Name not unique on network
77 EBADFD File descriptor in bad state
78 EREMCHG Remote address changed
79 ELIBACC Can not access a needed shared library
80 ELIBBAD Accessing a corrupted shared library
81 ELIBSCN .lib section in a.out corrupted
82 ELIBMAX Attempting to link in too many shared libraries
83 ELIBEXEC Cannot exec a shared library directly
84 EILSEQ Illegal byte sequence
85 ERESTART Interrupted system call should be restarted
86 ESTRPIPE Streams pipe error
87 EUSERS Too many users
88 ENOTSOCK Socket operation on non-socket
89 EDESTADDRREQ Destination address required
90 EMSGSIZE Message too long
91 EPROTOTYPE Protocol wrong type for socket
92 ENOPROTOOPT Protocol not available
93 EPROTONOSUPPORT Protocol not supported
94 ESOCKTNOSUPPORT Socket type not supported
95 EOPNOTSUPP Operation not supported on transport endpoint
96 EPFNOSUPPORT Protocol family not supported
97 EAFNOSUPPORT Address family not supported by protocol
98 EADDRINUSE Address already in use
99 EADDRNOTAVAIL Cannot assign requested address
100 ENETDOWN Network is down
101 ENETUNREACH Network is unreachable
102 ENETRESET Network dropped connection because of reset
103 ECONNABORTED Software caused connection abort
104 ECONNRESET Connection reset by peer
105 ENOBUFS No buffer space available
106 EISCONN Transport endpoint is already connected
107 ENOTCONN Transport endpoint is not connected
108 ESHUTDOWN Cannot send after transport endpoint shutdown
109 ETOOMANYREFS Too many references: cannot splice
110 ETIMEDOUT Connection timed out
111 ECONNREFUSED Connection refused
112 EHOSTDOWN Host is down
113 EHOSTUNREACH No route to host
114 EALREADY Operation already in progress
115 EINPROGRESS Operation now in progress
116 ESTALE Stale NFS file handle
117 EUCLEAN Structure needs cleaning
118 ENOTNAM Not a XENIX named type file
119 ENAVAIL No XENIX semaphores available
120 EISNAM Is a named type file
121 EREMOTEIO Remote I/O error
122 EDQUOT Quota exceeded
123 ENOMEDIUM No medium found
124 EMEDIUMTYPE Wrong medium type
125 ECANCELED Operation Canceled
126 ENOKEY Required key not available
127 EKEYEXPIRED Key has expired
128 EKEYREVOKED Key has been revoked
129 EKEYREJECTED Key was rejected by service
130 EOWNERDEAD Owner died
131 ENOTRECOVERABLE State not recoverable
Upvotes: 0
Reputation: 35540
One thing I've realized is that it is often not worth the trouble to percolate fatal errors up the stack. If something fails in a way that makes forward progress impossible, just end the program there. I usually deal with it by creating an error_exit
function that I can call from anywhere:
void error_exit(int code, const char* message) {
printf("Error %d: %s\nExiting!\n", code, message);
cleanup();
exit(code);
}
float* nested_function(int input, ...) {
if (causes_hopeless_failure(input)) {
error_exit(err_HOPELESS, "invalid input to nested_function");
}
//normal processing proceeds ...
return valid_pointer;
}
int main() {
float* vector = function_which_eventually_calls_nested_function();
cleanup();
return 0;
}
The cleanup
function is used to deal with resources that don't get cleaned up correctly on program exit. File handles and allocated buffers don't usually fall into this category. I generally use it for system configuration changes that need to be undone.
Upvotes: 0
Reputation: 140990
Don't return out of band error codes with pointers. A pointer is either NULL or valid (if it's not valid, usually stack overflowed in your program). Doing char *pointer = (char*)(uintptr_t)1;
is just confusing, and doing if ((uintptr_t)pointer == 1) {.. }
is as much un-maintainable.
Return an int
. int
is common in C standard library to return an error. Usually C libraries return -1
on error and set errno - I usually write library code that return a negative value that is an error code (ie. return -ENOMEM
in case of malloc
fail). Return 0
on success and maybe positive values to notify user codes on some "state" within the library. Pass all variables you want to set by a pointer. Look at ex. fopen_s (opinion: don't use fopen_s
, only look at it).
enum importantFunction_rets_e {
IMPORTANT_FUNCTION_ERR_1 = -1,
IMPORTANT_FUNCTION_ERR_2 = -2,
IMPORTANT_FUNCTION_STATE_1 = 1,
IMPORTANT_FUNCTION_STATE_2 = 2,
};
int importantFunction(char **pointer)
{
assert(pointer != NULL);
// or maybe
if (pointer == NULL) return -EINVAL;
int ret;
ret = secondFunction(pointer);
if (ret < 0) return ret;
ret = thirdFunction(pointer);
if (ret < 0) return ret;
return 0;
}
int secondFunction(char **pointer) {
*pointer = malloc(sizeof(char) * 5);
if (*pointer == NULL) {
return IMPORTANT_FUNCTION_ERR_1;
}
memcpy(*pointer, "hey!", 5);
return 0;
}
int main() {
char *pointer;
const int importantFunction_ret = importantFunction(&pointer);
if (importantFunction_ret < 0) {
if (importantFunction_ret == IMPORTANT_FUNCTION_ERR_1) {
// handle err 1
} else if (importantFunction_ret == IMPORTANT_FUNCTION_ERR_2) {
// handle err 2
} else {
// hanlde other errors
}
return -1;
}
if (importantFunction_ret == IMPORTANT_FUNCTION_STATE_1) {
// handle state1
} else if {importantFunction_ret == IMPORTANT_FUNCTION_STATE_2) {
// handle state2
} else {
// handle other states
assert(0);
}
}
If you feel like exploring the topic or error handling in C, you can implement something along the new (or is it old?) proposal that uses the same method as object oriented languages use std::variant
or std::expected
or similar (opinion: I am really against this proposal as it is right now, it needs redesign/refactoring, but it would be a BIG step forward for C).
Upvotes: 2
Reputation: 39326
There are three common patterns of returning an error from a function:
Have the function return an int
, with specific values indicating success and failure
For example, returning EXIT_SUCCESS
or EXIT_FAILURE
from main()
is the way the C standards recommend reporting success or failure of the entire process. (BSD variants have attempted to standardize some other codes; if your system has a <sysexits.h>
header, you could use those. But do note that they are not "standard", just the closest thing we have to an agreement how a process can report error codes.)
Reserve a specific return value for errors, and use a global or thread-local variable (usually errno
) to describe the error
Most standard C library functions do this, with functions returning int
using -1
for the error, and functions returning a pointer using NULL
to indicate the error.
Use an extra parameter to point to an error indicator.
This approach is common with code and interfaces deriving from Fortran. Often, the error indicator is optional, and may be left NULL
if the caller is not interested in whether the result is valid or not.
My own rules are simple:
Prefer the second approach when writing a low-level library. It is familiar approach to those familiar with the standard C library.
Use the first approach for recoverable errors.
Often, I combine it with the second one, using return 0;
for success, and return errno;
or return errno = EINVAL;
etc. for errors. (The last one first assigns EINVAL
to errno
, and then returns EINVAL
.)
Use the third approach when error state should be retained over a number of operations, or there is a structure whose state errors affect.
Let's look how these methods differ in practice.
A very common thing to do is to parse command-line arguments as numbers. Let's look at a case where the arguments are to be used as double
s, for some kind of calculation:
#include <stdlib.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
int arg;
double val;
for (arg = 1; arg < argc; arg++) {
if (sscanf(argv[arg], "%lf", &val) == 1) {
printf("argv[%d] = %.6f\n", arg, val);
} else {
printf("%s: Not a number.\n", argv[arg]);
exit(EXIT_FAILURE);
}
}
return EXIT_SUCCESS;
}
The above uses sscanf()
to convert a string. Unfortunately, it does not check for any trailing garbage, so it accepts for example 1.5k
as 1.5
. To avoid that, we can use a dummy character to detect trailing garbage:
#include <stdlib.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
int arg;
double val;
char dummy;
for (arg = 1; arg < argc; arg++) {
if (sscanf(argv[arg], "%lf %c", &val, &dummy) == 1) {
printf("argv[%d] = %.6f\n", arg, val);
} else {
printf("%s: Not a number.\n", argv[arg]);
exit(EXIT_FAILURE);
}
}
return EXIT_SUCCESS;
}
That works, because sscanf()
returns the number of successful conversions, and we expect only the double conversion (%lf
) to work, and the char conversion (%c
) to fail.
Unfortunately, scanf family of functions does not check for overflow. If you supply a large enough number, it gets silently mangled. Not good. To avoid that, we can use strtod()
. To make use simpler, we can put it in a separate function, parse_double()
. But, how should that return the value, and the possible error? Which one of the following to implement?
/* Convert the initial double, returning the pointer to the rest of the
string; or NULL if an error occurs. */
const char *parse_double(const char *src, double *to);
/* If the string contains exactly one double, convert it and return 0.
Otherwise return a nonzero error code. */
int parse_double(const char *src, double *to);
/* Convert the string to a double as best as you can. If an error occurs, return 'errval'. */
double parse_double(const char *src, const double errval);
So, which one of these is the best?
The answer is, of course, it depends on the use case.
I've actually implemented all three (in separate programs), depending on which one has been the most appropriate one.
The first one is especially useful when the same function is used to parse input files, and/or we allow any number of doubles per parameter/line. It is very easy to use in a loop.
The second one is what I most often use in programs. Very often, I use
typedef struct {
double x;
double y;
double z;
} vec3d;
int parse_vector(const char *src, vec3d *to)
{
vec3d temp;
char dummy;
if (!src || !*src)
return -1; /* NULL or empty string */
if (sscanf(src, " %lf %lf %lf %c", &temp.x, &temp.y, &temp.z, &dummy) == 3 ||
sscanf(src, " %lf %*[.,:/] %lf %*[.,:/] %lf %c", &temp.x, &temp.y, &temp.z, &dummy) == 3) {
if (to)
*to = temp;
return 0;
}
return -1;
}
which allows one to specify a 3D vector on the command line using 1+2+3
, 1/2/3
, 1:2:3
, or even '1 2 3'
or "1 2 3"
(the quotes are needed to stop the shell from splitting it to three separate arguments). It does not check for double
overflow, so it is important to show the parsed vector in the output, so that the user can detect if their input was misparsed.
(The asterisk *
in %*[.,:/]
means the result of the conversion is not stored anywhere, and the conversion is not counted in the return value. [
is a conversion specifier, "converting" any and all of the characters in the list, terminated with a ]
character. [^
is the inverse, "converting" any and all characters not in the list.)
Upvotes: 4
Reputation: 1059
A common way of handling errors in C is through return values.
Say a function f
, on success, returns a pointer to a character string.
char *f();
Such functions, upon failure, would return the NULL
pointer, which you can have by including some common header files (e.g. <string.h>
).
Now say g
is a function that given an integer, computes something and returns the integer result of the operation, but the function can fail (e.g. the parameter is invalid for the computation, who knows...). Then maybe you want to write it like so
int g(int i, int *result);
Here, i
is the parameter to compute something, and result
is a pointer to the variable you will use to store the result in. Now, why is g
's return type int
? Well, it could be bool
from <stdbool.h>
but usually one uses int
... The return value will be used as a boolean, g
will return 0
upon failure and 1
upon success.
You can use these like so in a third function h
int h(int i) {
char *str = f();
if (str == NULL) {
printf("f failed !\n");
return 0; // f failed
}
printf("%s\n", str);
int result;
if (!g(i, &result)) {
printf("g failed !\n");
return 0; // g failed
} else {
printf("result = %d\n", result);
}
return 1; // h success
}
Upvotes: 1