Delphi Compiler Directive to Evaluate Arguments in

2019-02-17 18:44发布

问题:

I was really impressed with this delphi two liner using the IFThen function from Math.pas. However, it evaluates the DB.ReturnFieldI first, which is unfortunate because I need to call DB.first to get the first record.

DB.RunQuery('select awesomedata1 from awesometable where awesometableid = "great"');
result := IfThen(DB.First = 0, DB.ReturnFieldI('awesomedata1'));

(as a pointless clarification, because I've got so many good answers already. I forgot to mention that 0 is the code that DB.First returns if it's got something in it, might not have made sense otherwise)

Obviously this isn't such a big deal, as I could make it work with five robust liners. But all I need for this to work is for Delphi to evaluate DB.first first and DB.ReturnFieldI second. I don't want to change math.pas and I don't think this warrants me making a overloaded ifthen because there's like 16 ifthen functions.

Just let me know what the compiler directive is, if there is an even better way to do this, or if there is no way to do this and anyone whose procedure is to call db.first and blindly retrieve the first thing he finds is not a real programmer.

回答1:

The evaluation order of expressions is commonly undefined. (C and C++ are the same way. Java always evaluates left-to-right.) The compiler offers no control over it. If you need two expressions to be evaluated in a specific order, then write your code differently. I wouldn't really worry about the number of lines of code. Lines are cheap; use as many as you need. If you find yourself using this pattern often, write a function that wraps it all up:

function GetFirstIfAvailable(DB: TDatabaseObject; const FieldName: string): Integer;
begin
  if DB.First = 0 then
    Result := DB.ReturnFieldI(FieldName)
  else
    Result := 0;
end;

Your original code probably wouldn't have been what you wanted, even if evaluation order were different. Even if DB.First wasn't equal to zero, the call to ReturnFieldI would still be evaluated. All actual parameters are fully evaluated before invoking the function that uses them.

Changing Math.pas wouldn't help you anyway. It doesn't control what order its actual parameters are evaluated in. By the time it sees them, they've already been evaluated down to a Boolean value and an integer; they're not executable expressions anymore.


The calling convention can affect evaluation order, but there's still no guarantee. The order that parameters are pushed onto the stack does not need to match the order in which those values were determined. Indeed, if you find that stdcall or cdecl gives you your desired evaluation order (left-to-right), then they are being evaluated in the reverse order of the one they're passed with.

The pascal calling convention passes arguments left-to-right on the stack. That means the leftmost argument is the one at the bottom of the stack, and the rightmost argument is at the top, just below the return address. If the IfThen function used that calling convention, there are several ways the compiler could achieve that stack layout:

  1. The way you expect, which is that each argument is evaluated and pushed immediately:

    push (DB.First = 0)
    push DB.ReturnFieldI('awesomedata1')
    call IfThen
    
  2. Evaluate arguments right-to-left and store the results in temporaries until they're pushed:

    tmp1 := DB.ReturnFieldI('awesomedata1')
    tmp2 := (DB.First = 0)
    push tmp2
    push tmp1
    call IfThen
    
  3. Allocate stack space first, and evaluate in whatever order is convenient:

    sub esp, 8
    mov [esp], DB.ReturnFieldI('awesomedata1')
    mov [esp + 4], (DB.First = 0)
    call IfThen
    

Notice that IfThen receives the argument values in the same order in all three cases, but the functions aren't necessarily called in that order.

The default register calling convention also passes arguments left-to-right, but the first three arguments that fit are passed in registers. The registers used to pass arguments, though, are also the registers most commonly used for evaluating intermediate expressions. The result of DB.First = 0 needed to be passed in the EAX register, but the compiler also needed that register for calling ReturnFieldI and for calling First. It was probably a little more convenient to evaluate the second function first, like this:

call DB.ReturnFieldI('awesomedata1')
mov [ebp - 4], eax // store result in temporary
call DB.First
test eax, eax
setz eax
mov edx, [ebp - 4]
call IfThen

Another thing to point out is that your first argument is a compound expression. There's a function call and a comparison. There's nothing to guarantee that those two parts are performed consecutively. The compiler might get the function calls out of the way first by calling First and ReturnFieldI, and afterward compare the First return value against zero.



回答2:

The calling convention affects the way they are evaluated.
There is not a compiler define to control this.

Pascal is the calling convention you would have to use to get this behavior.

Although I would personally never depend on this type of behavior.

The following example program demonstrates how this works.

program Project2;
{$APPTYPE CONSOLE}
uses SysUtils;

function ParamEvalTest(Param : Integer) : Integer;
begin
  writeln('Param' + IntToStr(Param) + ' Evaluated');
  result := Param;
end;

procedure TestStdCall(Param1,Param2 : Integer); stdCall;
begin
  Writeln('StdCall Complete');
end;

procedure TestPascal(Param1,Param2 : Integer); pascal;
begin
  Writeln('Pascal Complete');
end;

procedure TestCDecl(Param1,Param2 : Integer); cdecl;
begin
  Writeln('CDecl Complete');
end;

procedure TestSafecall(Param1,Param2 : Integer); safecall;
begin
  Writeln('SafeCall Complete');
end;

begin
  TestStdCall(ParamEvalTest(1),ParamEvalTest(2));
  TestPascal(ParamEvalTest(1),ParamEvalTest(2));
  TestCDecl(ParamEvalTest(1),ParamEvalTest(2));
  TestSafeCall(ParamEvalTest(1),ParamEvalTest(2));
  ReadLn;
end.

This would require you to write your own IfThen Functions.

If you really want this to be a one liner you really can do that in Delphi. I just think it looks ugly.

If (DB.First = 0) then result :=  DB.ReturnFieldI('awesomedata1') else result := 0;


回答3:

Can't you change your query to have only one result so avoid to do the 'First' command ? Just like :

SELECT TOP 1 awesomedata1 from awesometable 

In Access...



回答4:

AFAIK there is no compiler directive to control this. Unless you use the stdcall/cdecl/safecall conventions, parameters are passed left to right on the stack, but because the default register convention can pass parameters in the registers as well, it could happen that a parameter is calculated later an put in a register just before the call. And because only the register order is fixed (EAX, EDX, ECX) for parameters that qualify, registers can be loaded in any order. You could try to force a "pascal" calling convention (you'd need to rewrite the function, anyway) but IMHO is always dangerous to rely on such kind of code, if the compiler can't explicitly guarantee the order of evaluation. And imposing an evaluation order may greatly reduce the number of optimizations available.