snippetcMinor
Reasonable way to implement "safe" math operations using _Generic?
Viewed 0 times
operationsimplementwayreasonable_genericmathusingsafe
Problem
I've been thinking about a way to make it easier to safely use math operations with C's basic datatypes (e.g. using the CERT C coding standard). So far, I've come up with something like this:
Output of the above would be something similar to:
Integer wrap-around occurred, File: /.../main.c, Line: 41
and the program exits with a failure code (assuming the OS supports that).
Obviously, additional operations and types need to be added as well as handling signed arithmetic, but are there any problems with this overall method that I may be missing?
Is there perhaps an easier way to write this that I've overlooked?
#include
#include
#include
#define safe_add(x, y) _Generic((x + y), \
unsigned int: safe_add_uint, \
unsigned long: safe_add_ulong \
)(x, y, __FILE__, __LINE__)
unsigned long
safe_add_ulong(unsigned long x, unsigned long y,
const char* filename, int line_num)
{
if (x < ULONG_MAX - y)
return x + y;
else {
fprintf(stderr,
"Integer wrap-around occurred, File: %s, Line: %d\n",
filename, line_num);
exit(EXIT_FAILURE);
}
}
unsigned int
safe_add_uint(unsigned int x, unsigned int y,
const char* filename, int line_num)
{
if (x < UINT_MAX - y)
return x + y;
else {
fprintf(stderr,
"Integer wrap-around occurred, File: %s, Line: %d\n",
filename, line_num);
exit(EXIT_FAILURE);
}
}
int
main()
{
/*
* usual arithmetic conversions results in this calling
* the uint version of safe_add...
*/
safe_add(2000000000, 3000000000u);
printf("We shouldn't get here...(unless unsigned int uses more than 32 bits)\n");
}Output of the above would be something similar to:
Integer wrap-around occurred, File: /.../main.c, Line: 41
and the program exits with a failure code (assuming the OS supports that).
Obviously, additional operations and types need to be added as well as handling signed arithmetic, but are there any problems with this overall method that I may be missing?
Is there perhaps an easier way to write this that I've overlooked?
Solution
Very impressive! A few notes:
-
I really like how you are using C11 stuff.
It's good that you are using the more modern items the C standards have given us to use. My only nitpick with this
-
@Morwenn touches on your error handling a bit, but I want to go deeper. For now, I want to talk about the style of error reporting I've come to adopt for such APIs. Options for error reporting from C functions include the following:
-
Return an error code from functions that can fail.
-
Provide a function like Windows
-
Provide a global (well, hopefully, thread-local) variable containing the most recent error, like POSIX
-
Provide a function to return more information about an error, possibly in conjunction with one of the above approaches, like POSIX's
-
Allow the client to register a callback when an error occurs, like GLFW's
-
Use an OS-specific mechanism like structured exception handling.
-
Write errors out to a logfile,
-
Just
There are loads of tradeoffs among these options, and it's a matter of opinion as to which ones are better than others. Like all matters of style, unless a style is just plain unsuitable (like the last two options above might be for many uses), it's probably most important to apply the style you choose consistently.
Let me illustrate the style of error handling I use for C APIs with the following re-written function of yours:
Where we call it as:
Now your function can set the value of
You might think that this could be a bit cumbersome for an error handling system in C, but here's the catch: this C error handling style that plays nice with C++ exceptions too. This can be nice when implementing an hourglass API.
-
You should probably be using exact-width integer types.
-
Always declare what parameters your function takes in, even if nothing.
You might wonder why we have to do this. Imagine we have the function
In C, this is known as an identifier list and means that it "can take any number of parameters of unknown types". We can actually pass values to the function even though we don't mean to or intend to. If the caller calls the function giving it some argument, the behavior is undefined. The stack could become corrupted for example, because the called function expects a different layout when it gains control.
Using identifier lists in function parameters is depreciated. It is much better to do something like:
In C, this is known as a parameter type list and defines that the function takes zero arguments (and also communicates that when reading it) - like with all cases where the function is declared using a parameter type list, which is called a prototype. If the caller calls the function and gives it some argument, that is an error and the compiler spits out an appropriate error.
The second way of declaring a function has plenty of benefits. One of course is that amount and types of parameters are checked. Another difference is that because the compiler knows the parameter types, it can apply implicit conversions of the arguments to the type of the parameters. If no parameter type list is present, that can't be done, and arguments are converted to promoted types (that is called the default argument promotion).
-
I really like how you are using C11 stuff.
#define safe_add(x, y) _Generic((x + y), \
unsigned int: safe_add_uint, \
unsigned long: safe_add_ulong \
)(x, y, __FILE__, __LINE__)It's good that you are using the more modern items the C standards have given us to use. My only nitpick with this
#define is that your aligning is a bit off.#define safe_add(x, y) _Generic((x + y), \
unsigned int: safe_add_uint, \
unsigned long: safe_add_ulong) \
(x, y, __FILE__, __LINE__)-
@Morwenn touches on your error handling a bit, but I want to go deeper. For now, I want to talk about the style of error reporting I've come to adopt for such APIs. Options for error reporting from C functions include the following:
-
Return an error code from functions that can fail.
-
Provide a function like Windows
GetLastError() or OpenGL's glGetError() to retrieve the most recently occurring error code.-
Provide a global (well, hopefully, thread-local) variable containing the most recent error, like POSIX
errno.-
Provide a function to return more information about an error, possibly in conjunction with one of the above approaches, like POSIX's
strerror function.-
Allow the client to register a callback when an error occurs, like GLFW's
glfwSetErrorCallback.-
Use an OS-specific mechanism like structured exception handling.
-
Write errors out to a logfile,
stderr, or somewhere else.-
Just
assert() or somehow else terminate the program when an error occurs.There are loads of tradeoffs among these options, and it's a matter of opinion as to which ones are better than others. Like all matters of style, unless a style is just plain unsuitable (like the last two options above might be for many uses), it's probably most important to apply the style you choose consistently.
Let me illustrate the style of error handling I use for C APIs with the following re-written function of yours:
unsigned int safe_add_uint(unsigned int x, unsigned int y,
const char* filename, int line_num, char* error)Where we call it as:
char* error = /* allocate memory */;
safe_add_uint(x, y, __FILE__, __LINE__, &error)`Now your function can set the value of
error within the functions, and then print the output in something such as an atexit() function.You might think that this could be a bit cumbersome for an error handling system in C, but here's the catch: this C error handling style that plays nice with C++ exceptions too. This can be nice when implementing an hourglass API.
-
You should probably be using exact-width integer types.
-
Always declare what parameters your function takes in, even if nothing.
int main(void)You might wonder why we have to do this. Imagine we have the function
foo() declared as such:int foo()In C, this is known as an identifier list and means that it "can take any number of parameters of unknown types". We can actually pass values to the function even though we don't mean to or intend to. If the caller calls the function giving it some argument, the behavior is undefined. The stack could become corrupted for example, because the called function expects a different layout when it gains control.
Using identifier lists in function parameters is depreciated. It is much better to do something like:
int foo(void)In C, this is known as a parameter type list and defines that the function takes zero arguments (and also communicates that when reading it) - like with all cases where the function is declared using a parameter type list, which is called a prototype. If the caller calls the function and gives it some argument, that is an error and the compiler spits out an appropriate error.
The second way of declaring a function has plenty of benefits. One of course is that amount and types of parameters are checked. Another difference is that because the compiler knows the parameter types, it can apply implicit conversions of the arguments to the type of the parameters. If no parameter type list is present, that can't be done, and arguments are converted to promoted types (that is called the default argument promotion).
char will become int, for example, while float will become double.Code Snippets
#define safe_add(x, y) _Generic((x + y), \
unsigned int: safe_add_uint, \
unsigned long: safe_add_ulong \
)(x, y, __FILE__, __LINE__)#define safe_add(x, y) _Generic((x + y), \
unsigned int: safe_add_uint, \
unsigned long: safe_add_ulong) \
(x, y, __FILE__, __LINE__)unsigned int safe_add_uint(unsigned int x, unsigned int y,
const char* filename, int line_num, char* error)char* error = /* allocate memory */;
safe_add_uint(x, y, __FILE__, __LINE__, &error)`int main(void)Context
StackExchange Code Review Q#59557, answer score: 8
Revisions (0)
No revisions yet.