12

It looks like the Delphi compiler does not honor const record parameters when "records-with-methods" are involved.

Having not tried to abuse the const convention previously, I was a little surprised to find the compiler accepted code like that:

type
    TTest = record
       Field : String;
       procedure Update;
    end;

procedure TTest.Update;
begin
    Field := Field + '+1';
end;

procedure DoStuff(const t : TTest);
begin
    ShowMessage(t.Field);
    t.Update;
    ShowMessage(t.Field);
end;

While if you try to do a t.Field:='doh'; in DoStuff f.i., the compiler will properly complain, but you're allowed to call methods that modify the "const" record without even a hint or warning. So this is different behavior than for reference types (such as classes or dynamic arrays), where direct field writes are allowed (as const only restricts changes to the parameter itself).

Addendum: this allows to modify declared compile-time constants this way too, as in:

const
   cTest : TTest = (Field : '1');
...
cTest.Update;              // will show '1' then '1'+'1'
ShowMessage(cTest.Field);  // will show '1' (because optimized at compile-time)

Is that an accepted/documented behavior? or just a compiler shortcoming?

Eric Grange
  • 5,931
  • 1
  • 39
  • 61
  • I updated the addendum to call the method `Update` directly since it seems to me that is the fundamental issue here. You need a method of the record to mutate a const record. – David Heffernan Sep 14 '11 at 12:59
  • 3
    IMHO the real bug here is `Update`. [Value types should be used as immutable](http://blogs.msdn.com/b/ericlippert/archive/2008/05/14/mutating-readonly-structs.aspx). (Link is C# example, sorry, but the idea is the same.) See also [this SO answer](http://stackoverflow.com/questions/441309/why-are-mutable-structs-evil). – Craig Stuntz Sep 14 '11 at 13:42
  • @David, your edit to the addendum invalidates the example. Directly calling `Update` modifies the field value, but we don't get to see the result of that change because the compiler apparently optimizes the direct access to it in the subsequent `ShowMessage` statement. – Rob Kennedy Sep 14 '11 at 15:29
  • @Rob My edit was made before the point about `ShowMessage` and optimisation was made. At the time I made the edit, I believe it was reasonable. – David Heffernan Sep 14 '11 at 15:35
  • 1
    The DoStuff was in the sample *on purpose* before the edit, to show that in practice it can happen in situations where it's non obvious. – Eric Grange Sep 14 '11 at 15:42

2 Answers2

15

const never places any restrictions on method calls in Delphi, be they on records or instances of classes. So I don't think there is anything inconsistent with the treatment of method calls.

If methods could not be called on record passed as a const parameter, then that would pretty much render records with methods useless. It would mean, for example, that a property getter could not be called. In order to place restrictions on such records passed as const, there would need to be an equivalent concept to the const member functions of C++. That would allow the compiler to know that certain methods were non-mutating.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • For reference types that's expected, but this a value type, and the compiler will complain if you try to pass a const record to a procedure taking it as a var. Except for methods, which however take them as an implicit var. – Eric Grange Sep 14 '11 at 09:25
  • @Eric My point is that if a `const` record meant that no methods could be called, that would render methods on records useless. So there would need to be the equivalent concept of a C++ `const` member function. I've tried to explain my thought process better. – David Heffernan Sep 14 '11 at 09:28
  • 1
    The compiler could figure out for itself which methods are mutating, and which aren't, and adapt the hidden call convention accordingly. As it is, the 'const' specifier is rendered useless. – Eric Grange Sep 14 '11 at 09:41
  • @Eric That may be possible but would surely incur a heavy penalty on the compiler. – David Heffernan Sep 14 '11 at 09:53
  • 1
    This allows very bad things however, as in modifying compile-time declared constants (I've updated the question with an example) – Eric Grange Sep 14 '11 at 12:38
  • It would be nice if the compiler would have put `cTest` in read-only memory. Essentially though, this whole issue is what happens to you if you allow objects with mutable state. Even the more advanced const features in C++ (compared to Delphi) are still just a sticking plaster. The only really good solution (in terms of language design) is objects with immutable state. In the meantime you can go down this road by making your records immutable. – David Heffernan Sep 14 '11 at 12:56
  • This plays nice tricks along with the compiler optimizations (ipdated the sample again). Short of manual code review, you can't guarantee that a record is immutable as soon as it involves a method, as any method can mutate the record without any hint or warning. – Eric Grange Sep 14 '11 at 13:05
  • @Eric In native code with pointers, what you say is correct. But you can write immutable records, so long as nobody starts casting, and casting is naughty isn't it. Really the only good solution is language/runtime supported immutability. – David Heffernan Sep 14 '11 at 13:09
  • You can, but with the weakening of "const", the Delphi language doesn't provide you with ways to enforce or guarantee it. As in the sample I posted, you don't have to involve *any* pointer. – Eric Grange Sep 14 '11 at 13:45
  • @Eric Not true. Use `strict private` for the fields and only assign to those fields in constructors. Don't have methods that mutate records. – David Heffernan Sep 14 '11 at 13:50
  • David that's incorrect: "strict private" only restricts scope to methods (down from everywhere in the unit implementation), you still have to enforce the rule of no mutation outside of constructors manually, ie. the language doesn't support it. – Eric Grange Sep 14 '11 at 15:14
  • @Eric Yes, no language support for that. That's down to you. But at least it's practical for you to verify it since your record has typically a small number of methods that are located next to each other in source. Language support for immutable objects is what you want but that's not very common in traditional programming languages. – David Heffernan Sep 14 '11 at 15:16
  • 1
    It seems the OP would like all called procedures to have a "`nosideeffects;`" decoration, or something, and then have the compiler enforce that? – Warren P Sep 14 '11 at 15:36
  • 2
    @Warren That's what C++ const member functions do. – David Heffernan Sep 14 '11 at 15:37
  • 1
    @Eric: I fully agree with David. Unlike in C++, there are no `const` methods in Delphi, so any method call is allowed. Only direct modification of the record is not allowed. You could suggest `const` methods to Embarcadero, but ATM, they don't exist, and that is, IMO, fine. I don't expect the compiler to analyze every method you call to see to see if it modifies the state of the const parameter. – Rudy Velthuis Sep 14 '11 at 20:33
  • @EricGrange It was always possible to modify ANY field in the constant record with trivial construction `PMyRec(@MyRec).Field := ...` so I don't get the reasons of your panic. – Fr0sT Nov 05 '14 at 16:08
  • 1
    @Frost pointer tricks do not count, just like editing the binary with an hex editor doesn't count. – Eric Grange Nov 06 '14 at 16:42
4

David analyzed the restriction pretty well. If the compiler was to check out such details it could really do it with some penalty. Additionally I don't see anything wrong with the compiler's behaviour. The method which gets the record can't directly alter its data, but only when using the method it contains. The record in this case works like an object: you can in the same way an object as a const and still have the same problem you described, ie. the object's methods can be used to alter its data.

The benefit of the object is, that such methods can be declared to be private, which enables you to protect its data. You could even create an inherited class which does just that, namely hiding all possibility to alter its data. Maybe you want to try this approach?

Fotis MC
  • 323
  • 1
  • 2
  • 12
  • Yes, but objects are reference type, records are value type. It looks like property offer a similar bypass for "const", I guess the conclusion is that "const" is pointless for records too, except for call optimization. – Eric Grange Sep 14 '11 at 10:28
  • Right. But it's not exactly pointless using "const" for records, particularly if they don't contain any methods. – Fotis MC Sep 14 '11 at 10:30
  • Note that it affects compile-time constant records too, kinda like reviving the "writable constants" compiler option. – Eric Grange Sep 14 '11 at 12:40
  • Try it with old style object types. I expect the same behaviour as with records. And as I said in another comment, that is fine. – Rudy Velthuis Sep 14 '11 at 20:35