2

I have a function which takes in a mathematical operation in the form of a char array, returning an int result (this all works, and is purely for context, not really related to the question).

Naturally, my function definition is: int calc(char* operation) {} which expects an int to be returned.

After parsing the string to determine the operands and the operation to be performed, I assign the result to a variable. I just realized that I had forgotten to put the return statement in the function, and yet I was still getting the correct results...

Here is the function. I had forgotten the last line originally.

// Function to return int results of operation specified in char* (argv[1])
int calc(char* operation)
{
    int op_index = 0;
    int end_index = 0;
    for (int i = 0; i < 128; i ++)
    {
        if ((operation[i] < 48 || operation[i] > 57) && op_index == 0)
            op_index = i;
        if (operation[i] == '\0')
        {
            end_index = i;
        i = 128;
        }
    }

    int opa = 0;
    int opb = 0;
    for (int i = 0; i < op_index; i ++)
        opa += (operation[i]-48)*power(10, op_index - (i+1));
    for (int i = op_index+1; i < end_index; i ++)
        opb += (operation[i]-48)*power(10, end_index - (i+1));

    int res = 0;
    if (operation[op_index] == '+')
        res = opa + opb;
    else if (operation[op_index] == '-')
        res = opa - opb;
    else if (operation[op_index] == '*')
        res = opa * opb;
    else if (operation[op_index] == '/')
        res = opa / opb;
    else if (operation[op_index] == '%')
        res = opa % opb;

    // This is the line that I had forgotten... but still got the right results when calling this function
    return res;
}

Does anyone have an explanation for this? My guess is that it is returning, by default, the result of the last function call, which would be correct because of the if/else structure of the final statements.

Thanks!

Spencer B.
  • 43
  • 5
  • Not having `return` on some path in a non-void function is undefined behavior. – Eugene Sh. Mar 04 '19 at 19:58
  • With undefined behavior it's tough to tell why this worked because it's dependent on your specific compiler and machine. It's important to understand that this behavior can't be relied upon in future calls. If you're curious and want to dig deeper anyway, you could probably gain some insight by looking at this function's assembly. – Reticulated Spline Mar 04 '19 at 19:58
  • It would be better to use `(size_t n, char operation[n])` instead of `(char *operation)` if you don't know the size of the input string, or `(char (*operation)[N])` if you know it at compile time. The first method tells you the size of the input buffer. The second one is stronger, as it won't compile if the input buffer is of different size, and thus is safer, but it is only so if the array size is known at compile time. Also as the latter is not an array, but a pointer to array, the usage would differ. – alx - recommends codidact Mar 04 '19 at 20:02
  • A simple view is that call and return is implemented by pushing parameters onto a call stack, and then popping the result off of the call stack.Because you left out the return statement in the `calc` function, there was nothing put onto the call stack in the location that the calling function would look for. So, then when the calling function popped the stack looking for the result, it turned out that the data on the stack happened to be the last variable that you declared, which in this case is `res`. But, it could have been some other variable, or you could crash accessing unavailable info – bruceg Mar 04 '19 at 20:10

2 Answers2

6

Technically undefined behavior.

If this is x86 Intel, what is likely happening is that the math operations performed prior to returning from the function are just happening to be leaving the intended return value in the EAX register. And for functions that return integers, the EAX register is also how the return value gets passed back to the caller.

Tail end of your calc function has generated assembly that looks like this:

    int res = 0;
 mov         dword ptr [res],0  
    if (operation[op_index] == '+')
 mov         eax,dword ptr [operation]  
 add         eax,dword ptr [op_index]    // MATH OPERATION WINDS UP IN EAX REGISTER
 movsx       ecx,byte ptr [eax]  
 cmp         ecx,2Bh  
 jne         calc+149h (05719F9h)  

And invoking code like this:

int x;
x = calc((char*)"4+5");
printf("%d\n", x);

Generated assembly is this

    x = calc((char*)"4+5");
 push        offset string "4+5" (0E87B30h)  
 call        _calc (0E8128Ah)  
 add         esp,4  
 mov         dword ptr [x],eax   // TAKE EAX AS RESULT OF FUNCTION AND ASSIGN TO X

But the moment I switch the project settings from debug build to optimized retail, all bets are off. The compiler and linker will start inlining assembly, making crazy optimizations, etc... and it will even optimize around the fact that the function isn't returning anything... As a matter of fact, it will generate an error near the printf statement complaining that x is uninitialized even though it explicitly was assigned to from the result of calc.

So the short answer is that you are getting lucky. But I wanted to point how why it "just happens to work".

selbie
  • 100,020
  • 15
  • 103
  • 173
  • Thanks for the detailed answer. Interestingly, it worked for all operations except mod. – Spencer B. Mar 04 '19 at 20:53
  • @SpencerB - That's because mod operator is compiled into an `idiv` (integer division) instruction and gets the remainder from the EDX register. So without a return value, you get the result of the division operation. **Hence, undefined behavior** – selbie Mar 04 '19 at 21:02
  • This was the exact result I saw - kept getting the division result. Consequently, this was the only reason I found the error. – Spencer B. Mar 04 '19 at 21:11
  • I'm actually surprised you didn't get a compiler warning about a function missing the return value. On GCC, you can use `-Wall -Wuninitialized` as compiler options. Another thing I recommend is that just compiling your C code as C++ sometimes brings about better type checking and warnings to be found. – selbie Mar 04 '19 at 23:22
5

With the exception of the main function, any function that is defined to return a value must do so. If it does not, and the calling function attempts to use the returned value, you invoked undefined behavior.

This is specified in section 6.9.1p12 of the C standard:

If the } that terminates a function is reached, and the value of the function call is used by the caller, the behavior is undefined.

In this case, you got "lucky" that the program happened to work, but there's no guarantee that will always be the case. A seemingly unrelated change to your program can change how undefined behavior will manifest itself.

dbush
  • 205,898
  • 23
  • 218
  • 273