3

I have the record type:

type
  TIPInfo = record
    IP,
    HostName,
    City,
    Region,
    Country,
    Loc,
    Org: WideString
  end;

Function to return the record data:

function GetPublicIPInfo(var IPInfo: TIPInfo): Boolean;
begin
  // initialize
  FillChar(IPInfo, SizeOf(TIPInfo), 0);

  // populate data
  IPInfo.IP := GetVallue('ip');
  IPInfo.HostName := GetVallue('hostname');
  IPInfo.City := GetVallue('city');
  // etc...

  Result := IsOk;   
end;

The caller:

var
  IPInfo: TIPInfo;

if GetPublicIPInfo(IPInfo) then... // use data

Is it the correct way to initialize the var TIPInfo by calling FillChar or should I set every field to empty string? Should the caller do that?

Also, would it be better to use an out parameter in the case (since the function is not reading the data)?

13
  • I would use an out parameter when the function does not read the parameter. Also, in that case it is clear that function itself should initialize it and not the caller. FillChar does not properly initialize managed types like strings in every situation (although it does in your case), so I would just initialize every field separately. Commented Jan 26, 2017 at 15:43
  • 2
    In this case, all of the record fields are WideString, which is auto-initialized by the compiler, so the function does not need to initialize anything at all. That being said, I agree with R.Beiboer, using out instead of var is the best choice. Commented Jan 26, 2017 at 15:57
  • @RemyLebeau The members could have arbitrary values from some previous use of that object. So they do need to be assigned. Commented Jan 26, 2017 at 15:59
  • @R.Beiboer, What do you mean by "although it does in your case"? Commented Jan 26, 2017 at 16:11
  • 1
    @zig When all your WideStrings are empty strings, FillChar does not harm. If any of them are assigned, FillChar does not dispose the strings. I assumed that in your case they are empty, that is why I said "although it does in your case". Furthermore, I think you should accept David's answer because he clearly explains how initializing records works and the usage of out and var. Commented Jan 27, 2017 at 7:51

1 Answer 1

5

Using just FillChar is wrong here. If any of the WideString members are not empty, then you will leak them this way. Instead I suggest the following:

Finalize(IPInfo);
FillChar(IPInfo, SizeOf(TIPInfo), 0);

Or another way is to define a default record as a typed constant:

const
  DefaultIPInfo: TIPInfo = ();

Then you can use simple assignment:

IPInfo := DefaultIPInfo;

In modern versions of Delphi you can instead use this much more readable code:

IPInfo := Default(TIPInfo);

For more on this particular subject, refer to these topics:

Note that the leak in your code is hard to find because WideString variables are implemented as COM BSTR objects, and allocated on the COM heap. Therefore, if you use memory leak detection for the Delphi memory manager the leak will not be detected because it is leaked from a different heap.

In your case, because your record is a managed type, and contains only managed types, then you could use an out parameter to good effect. For managed types, out parameters mean that the compiler will generate code, at the call site, to default initialize the record before passing it in.

Consider the following program:

{$APPTYPE CONSOLE}

type
  TRec = record
    Value: WideString;
  end;

procedure Foo1(var rec: TRec);
begin
end;

procedure Foo2(out rec: TRec);
begin
end;

procedure Main;
var
  rec: TRec;
begin
  rec.Value := 'Foo';
  Foo1(rec);
  Writeln(rec.Value);
  Foo2(rec);
  Writeln(rec.Value);
end;

begin
  Main;
end.

The output is:

Foo

If your record contains a mix of managed and unmanaged types, then the situation is not so good.

{$APPTYPE CONSOLE}

type
  TRec = record
    Value1: WideString;
    Value2: Integer;
  end;

procedure Foo1(var rec: TRec);
begin
end;

procedure Foo2(out rec: TRec);
begin
end;

procedure Main;
var
  rec: TRec;
begin
  rec.Value1 := 'Foo';
  rec.Value2 := 42;
  Foo1(rec);
  Writeln(rec.Value1);
  Writeln(rec.Value2);
  Foo2(rec);
  Writeln(rec.Value1);
  Writeln(rec.Value2);
end;

begin
  Main;
end.

The output is:

Foo
42

42

Only the managed members are default initialized for out parameters. So your best bet is to default initializing the variable yourself, even if it is passed as an out parameter.

More on out parameters can be found here: What's the difference between "var" and "out" parameters?

Sign up to request clarification or add additional context in comments.

5 Comments

So you suggest to always call Finalize(IPInfo); FillChar(IPInfo, SizeOf(TIPInfo), 0);?
My opinion is that you should write the code so that it is robust to future changes. And that means default initializing the record always, even if it happens to contain only managed types. If you take the record in the question, then you can use an out parameter and rely on the compiler to default init at the call site. But then one day in the future you will add a non-managed member to the record, and some function miles away will break. My opinion is that out is close to useless.
I'm asking again: can I always call Finalize(IPInfo); FillChar(IPInfo, SizeOf(TIPInfo), 0); as a general solution? is there a general solution at all?
Yes, I said that you can do that. That's clear in the answer, and in my comment above.
Great tip with the typed constant!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.