Search code examples
c++compiler-errorsglobal-variablesc-preprocessor

Is there a way to flag a global variable being set in multiple places in c++?


I'm looking for a way to, at compile time, flag/throw an error if a particular variable is set by more than one line in C++.

i.e. something like:

int a; // ~special~

int main(){
    while(1){
        fn1();
        fn2();
        print(a);
    }
}

void fn1(){
    a++;
}

void fn2(){
    print("hi");
}

This should compile fine, BUT changing fn2() to:

void fn2(){
   a = 2;
}

should throw an error, since a is modified by both fn1 and fn2.

Note a must be modified often, at runtime (so setting it as a const, or making a function that locks after setting it once at runtime, does NOT work). It just shouldn't be modified by more than a single function.

Note that this MUST be done at compile-time, as ANY chance of a "collision" means a serious bug. It CAN be assumed that no two threads/cores will ever call the same function that sets a variable (i.e. if thread1 calls fn1, it can be assumed thread2 will NEVER call fn1), so it is sufficient to check that a variable is only written to on one line.

This is embedded and on a very slow processor, run-time checks will add too much overhead, as well.

Sorry if this is a repeat question, I did search, but everything was for setting a variable only once (i.e. const), not on only one line.


(the rest of this question is just context, skip if you've already got an answer)

A critique of "you shouldn't be doing that!" "global variables bad!" or something isn't helpful - this is a very specific situation (multi-core bare metal real-time embedded software, whose execution path must change completely based on any input from several different cores/threads) - it's kind of gross but something like this IS the cleanest way to do this.

Note that working around this with something like:

void fn1(){
   modifyA(1);
} 

void fn2(){
   modifyA(2);
} 

void modifyA(int in){
   a = in; 
}

does NOT work (though it's fine if the answer to this question WOULDN'T catch this).

However, something like:

#define setA(in) *some preprocessor magic*(a=in)*more magic*

(so maybe something like #ifdef A_IS_SET #error "a set twice" #else #define A_IS_SET ; a=*whatever* #endif? But that doesn't quite work)

where having setA() twice in the program would cause an error.

The reason for this is there's a huge global state vector (~100 variables), each of which should only ever be SET by one single core/thread, but needs to be read by all.

Each core/thread has a local "copy" of the global state, and there's a single mutex for the global state. Each core will grab the mutex, modify some variables in the global state, copy everything else in it to its local copy (so it's synchronized with the global state), then release the mutex. This may happen at any time, in any order (so core 0 might do it five times in a row, then core 2, then 0 again, then 1, or whatever).

Simplified example:

int a;
int b;
int c;
float d;
bool e;
mutex_t globalMutex;

int core0_a; //"coren_x" is local copy of global var x, for core/thread n (yes this part is handled better in the real thing, simplified here)
...
bool core0_e;
int core1_a;
... (and so on for all other cores/vars)

fnRunByCore0(){
    getMutex(globalMutex);
    a = core0_a;                     //so a is now "owned" by core0
    c = core0_c;                     //so c is now "owned" by core0 too

    core0_b = b;
    core0_d = d;
    core0_e = e;
    releaseMutex(globalMutex);
}

fnRunByCore1(){
    getMutex(globalMutex);
    b = core1_b;                     //so b is now "owned" by core1
    d = core1_d;                     //so d is now "owned" by core1

    core1_a = a;
    core1_c = c;
    core1_e = e;
    releaseMutex(globalMutex);  
}

fnRunByCore2()
... (same deal, but sets only e)

The reason for this question is to make sure that I'm keeping track of which global vars are written by each core/thread/function. In theory, I should just need to read each of those functions and make sure that they're each only set once. In practice, that's a LOT of checking by eye, this is just to double-check.

The hardware doesn't have the performance overhead available to keep something like an index that's checked at runtime for what core owns each variable, or individual mutexes, or anything like that - this is run ~100 types per loop, and any runtime code will make performance unacceptable. Besides, this indicates a bug in the program, so should be caught at compile-time anyway.


Solution

  • It is possible if you don't mind adding some header to each of the functions you want to check, and the functions are all in the same TU.

    First, we define a macro to obtain a unique type from each function.

    #define DECLARE_ONE_PLACE_CHECK using _globalOnceCheck = decltype([](){})
    

    Then, we abuse the rule that a single global variable cannot be declared twice with different types.

    #define SET_ONE_PLACE(var, val) do{extern _globalOnceCheck checkUseOnce##var; var = val;}while(0)
    

    Now, if you do,

    void f1(){
        DECLARE_ONE_PLACE_CHECK;
        SET_ONE_PLACE(a, 42);
        SET_ONE_PLACE(a, 4242)
        if(true){
            SET_ONE_PLACE(a, 424242);
        }
    }
    

    The compiler will happily accept it, meaning you can call it multiple times in the same function, regardless of the scope.

    However, if instead you have

    void f1(){
        DECLARE_ONE_PLACE_CHECK;
        SET_ONE_PLACE(a, 42);
    }
    
    void f2(){
        DECLARE_ONE_PLACE_CHECK;
        SET_ONE_PLACE(a, 42);
    }
    

    The compiler will complain about redeclaration of checkOneUsea as a different type, because the type of lambda is unique to each instance.

    Despite the the single-TU restriction (which I admit is a rather strong limitation, but anyway has been confirmed to work in OP's case), this solution is truely zero-cost in run time. All the decltype and extern declaration stuff are purely compile time, and the check in do while(0) can be optimized away even by the dumbest compiler, so it really the go-to solution for real-time systems unless a more general and expressive one exist.

    If the check really need to spread over multiple TUs, I think we can make a file that combines all the potentially problematic source files to a single one by using #include, and then try to compile the resulting file just for the sake of checking, That file should of course be excluded from the actual main build process. I am aware that #includeing multiple source files is not always the same as having multiple TUs, especially if static is involved. However, I believe it's something worth trying and should work in many cases.