Passing parameters - michaliskambi/modern-pascal-introduction GitHub Wiki

When passing a parameter A to a function / procedure / method, you can declare how it is passed:

procedure DoSomething(A: TSomeData);
procedure DoSomething(var A: TSomeData);
procedure DoSomething(const A: TSomeData);
procedure DoSomething(out A: TSomeData);
procedure DoSomething(constref A: TSomeData);

Various ways to pass the parameter determine:

  • Whether you can change the value of A inside DoSomething (in case of const and constref you cannot),
  • What happens with the variable that was used to initialize the parameter, when you change the A inside DoSomething.
  • How is the A internally passed to the procedure (by copying the value, or by passing a pointer to the value).

The full explanation of this may get a little complicated if you consider all the possible things that TSomeData may be (simple integer/float/enum, a class, a record, an AnsiString...), I tried to keep my answer below simple :) More information e.g. in FPC docs on https://www.freepascal.org/docs-html/current/ref/refse91.html

Passing the value (copy the value being passed to the procedure)

procedure DoSomething(A: TSomeData);

When called as DoSomething(B), the compiler creates a copy of B and places it inside your parameter A. This allows you to freely change the value of A inside the DoSomething implementation, without affecting B.

E.g.

procedure DoSomething(A: Integer);
begin
  A := A + 10;
  Writeln(A);
end;

var
  B: Integer;
begin
  B := 20;
  DoSomething(B); // will write 30
  // B is still 20 here
end.

This is sometimes useful, if you really want to modify A inside DoSomething, without affecting the B. But if you don't want to modify A inside the DoSomething implementation, then using const is usually safer (compiler will not let you modify A inside the procedure) and may be more efficient (the const parameter may be internally passed by only passing a pointer to it, not copying it's value, in case if TSomeData size is larger).

Note that if TSomeData is a class (like TObject, TStringList...) then we're only talking here about copying the reference to the class contents. A and B will still point (and least initially) to the same instance. Things are different in case TSomeData is a record, in which case A and B will be just different values. The rule to remember here is that this "copy" is exactly the same as when you do A := B in code.

As far as efficiency, the compiler must create a copy of the value. So this is not efficient for larger TSomeData (when TSomeData is something larger than a pointer) -- well, unless you really wanted to create a copy, to be able to modify it locally inside DoSomething.

Passing by a pointer to the variable

procedure DoSomething(var A: TSomeData);

When called as DoSomething(B), the compiler internally passes a pointer to B. Accessing A (reading, writing) is now the same thing as accessing B. You can change A inside DoSomething implementation, and it immediately changes also B.

procedure DoSomething(var A: Integer);
begin
  A := A + 10;
  Writeln(A);
end;

var
  B: Integer;
begin
  B := 20;
  DoSomething(B); // will write 30
  // B is now 30
end.

So this is very efficient (only a pointer is passed, regardless of what TSomeData is). Whether you want it -- depends on what you wanted to achieve.

Constant parameter (cannot modify it inside the procedure, doesn't matter how it's internally passed)

procedure DoSomething(const A: TSomeData);

In this case, you cannot change A inside DoSomething.

It is efficient, and the compiler decides whether internally a pointer to TSomeData is passed, or a just a copy of TSomeData is made. In this sense, the compiler internally decides between doing something like option 1. (copying the value, which makes sense if it's something smaller than the pointer) or 2. (passing the pointer to the value). But what you can do with A differs from both option 1. and option 2. (as now you cannot modify "A" at all inside DoSomething).

Some examples when it is more efficient:

  • Passing a reference-counted type (like AnsiString or UnicodeString) using const is more efficient, because it avoids incrementing/decrementing internal reference-count.

  • Passing a larger record, that size > pointer size using const is more efficient, because less data needs to be copied. E.g. CGE TVector3 (record) is 12 bytes (3 floats). Passing it using const is a bit more efficient, because it means only 8 bytes (on 64-bit CPUs) or 4 bytes (or 32-bit CPUs) need to be copied. The difference is even more drastic if you think about CGE TMatrix4 (16 floats, so 16*4 = 64 bytes) type.

  • Passing a method callback (procedure (...) of object) is also more efficient. Internally a method callback size is 2 * pointer. Passing it using const means that we copy SizeOf(Pointer) bytes, not 2 * SizeOf(Pointer) bytes.

Note that all these performance savings are typically extremely tiny. Unless we consider some really special routine which "bottleneck", the practical speed difference between const and not-const is probably unmeasurably small. Still, the additional benefit is the compiler check: you cannot modify a const parameter by accident. This is useful, naturally when you really didn't indent to modify it.

Out parameter, that must be set inside the procedure implementation

procedure DoSomething(out A: TSomeData);

out A is similar to var A, but it tells the compiler that the initial value of the parameter (when the DoSomething is called) doesn't matter, and the implementation of DoSomething should always set it. It will also change the warnings/hints generated by the compiler.

It may make the parameter passed differently, but in practice it is always passed internally using a pointer. So this is as efficient as option 2., var A.

Note that if a procedure has only a single out parameter, then it's usually more comfortable to just define it as a function: function DoSomething: TSomeData;. Using out is more useful if you want to return two or more things.

Constant parameter that must be passed by a pointer

procedure DoSomething(constref A: TSomeData);

constref A is like const A, but forces the compiler to pass a pointer to A internally. This is usually not needed (using "const" leaves compiler more flexibility, and is usually as good for you). This is as efficient as option 2., var A.

(I originally wrote this text as an answer to forum question here: http://forum.lazarus.freepascal.org/index.php?topic=37980 .)