Jump to content

Sky Slate Blueberry Blackcurrant Watermelon Strawberry Orange Banana Apple Emerald Chocolate
Photo

Suggestion: issue a warning when accessing uninitialised class static data members


  • Please log in to reply
1 reply to this topic
Picotin
  • Members
  • 1 posts
  • Last active:
  • Joined: 02 Jun 2015

A few years ago, Lexikos wrote:

Static declarations are processed in the order that the pre-parser encounters them - top to bottom. Static declarations are not subject to control flow.

 ("http://www.autohotke...ed/#entry552863")

I accept that this behaviour is either by design (i.e., it’s somehow preferable) or required by internal limitations that would be difficult to eliminate.

Nonetheless, I find it rather unintuitive, enough so to warrant a warning being issued when the initialisation of a static variable references another static that has not yet been initialised. Such a warning is indeed issued for function statics but apparently not for class static data members.

 

Here is an example:

#Warn All , MsgBox

Show( vLocation , vName , vValue ) {
  MsgBox , % Format( "Location #{1:d}: {2:s} = {3:d}"
                   , vLocation
                   , vName
                   , vValue )
}

; Test case #1: accessing a function's uninitialised static local variable.

f1() {
  static v1 := f2( 1 )
  Show( 2 , "f1.v1" , v1 )
}

f2( vLocation ) {
  static v1 := 123
  Show( vLocation , "f2.v1" , v1 )   ; line 019: warning.
  return v1
}

f3() {
  static v1 := f2( 3 )
  Show( 4 , "f3.v1" , v1 )
}

; Test case #2: accessing a class' uninitialised static data member.

f4() {
  static v1 := (new C1( 5 )).p1
  Show( 6 , "f4.v1" , v1 )
}

class C1 {
  static k1 := 123
  __New( vLocation ) {
    base.__New()
    Show( vLocation , "C1.k1" , C1.k1 )   ; No warning here.
  }
  p1 {
    get {
      return C1.k1
    }
  }
}

f5() {
  static v1 := (new C1( 7 )).p1
  Show( 8 , "f5.v1" , v1 )
}

MsgBox , % "All statics have been initialised."
f1()
f3()
f4()
f5()
return

This produces the following output:

Warning: line 019: v1 (a static variable) has not been assigned a value.
Location #1: f2.v1 = 0
Location #3: f2.v1 = 123
Location #5: C1.k1 = 0
Location #7: C1.k1 = 123
All statics have been initialised.
Location #2: f1.v1 = 0
Location #4: f3.v1 = 123
Location #6: f4.v1 = 0
Location #8: f5.v1 = 123

The output is consistent with the assertion that static definitions are processed in parsing order and not based on execution flow.

However, note the warning issued about ‘f2.v1’ (line 019): the parser has detected that the static variable is read before its definition is processed (coming from location #1). The warning may be a bit confusing (for those who expect static definitions to follow execution flow) but still helpful (perhaps a more specific warning could be substituted in the future).

On the other hand, no such warning was issued for ‘C1.k1’ at location #5. Isn’t it essentially the same situation? The static was read before being assigned a value, hence the zero value. So a similar warning would have been helpful.

 

Thanks!



Lexikos
  • Administrators
  • 9844 posts
  • AutoHotkey Foundation
  • Last active:
  • Joined: 17 Oct 2006
The #Warn warning only applies to local and global variables, not "variables" in objects, which are really just associative array elements. It applies to static and non-static variables equally. Technically all variables and array elements are initialized (if they exist), but variables have an additional "uninitialized" flag which is unset when the script assigns it a value or calls VarSetCapacity. Array elements do not have this flag.
 
There are a few reasons for the order of initialization:
  • It's by far the simplest way.  Any other order quickly gets complicated.  For instance, if static variables should be initialized the first time control flows over their declaration (as for static local variables in C++), what about static class variables?  Control never flows over those.  Also, it's possible to reference a local static variable which control has never "flowed over", since local variables are scoped to the function in AutoHotkey, not the block as in C++.
  • It allows static initializers to be used as independent "auto-execute" expressions, as a substitute for code in the script's auto-execute section. This is particularly useful in libraries or other #include files.
  • I suspect that initializing in the order first encountered during execution would still fail when there are circular dependencies, only it would be even harder to grasp.
It's possible to modify the behaviour of a class by defining meta-functions in a super-class (not the class itself), such as to detect the use of unknown static members:
Spoiler
 
However, a class-static declaration like static x := 1 prevents Class.x from triggering meta-functions, by design. A declaration without an initializer actually has no other effect (aside from load-time duplicate detection), so isn't really necessary. Instead, you can use an initialization method. For example:
Spoiler

However, if class C uses class D, which uses class C, we're back to the problem that some static variables may be referenced before they're initialized.  I suppose that a warning would only help you see that there's a design error in your script; you still need to fix the design.  I'm not sure what the best solution is, but this might help:
Spoiler

 
In order to identify static properties prior to initializing them, you would need to list them separately to the initializers.  AutoHotkey basically does this by storing key-value pairs (with empty value) in the class object during parsing.