Tag: variables

  • Mastering Variable Scopes in C/C++: Best Practices for Clean and Effective Code

    As software developers, we rely on variables to store and manipulate data in our programs. However, it is crucial to understand the scope of a variable and how it affects its accessibility and lifetime. In C and C++, the scope of a variable determines where in the program it can be used and for how long it will exist. In this blog post, we will be exploring the different types of scopes in C/C++ and the best practices for handling them to write clean, maintainable, and effective code.

    We will look at global, local, and member scopes and how they affect the lifetime of variables. We will also discuss how to properly handle pointers, which have their own unique set of considerations when it comes to scope. By understanding the different types of scopes and how to handle them, you will be equipped to make conscious decisions about how you use variables in your code, leading to more reliable, efficient, and maintainable programs.

    Variable Scope Awareness

    Awareness of variable lifetimes and scopes, particularly when it comes to pointers, is critical to writing clean and effective C/C++ code. The lifetime of a variable is the period of time during which it is allocated memory and exists in the program. In C/C++, variables can have three different scopes: global scope, local scope, and member scope.

    Global Scope Variables

    Global scope variables are declared outside of all functions and are accessible throughout the entire program. They have a longer lifetime and persist throughout the execution of the program, but using too many global scope variables can lead to cluttered code and potential naming conflicts. However, in my mind, the more serious implications of improper use of a global variable is race conditions.

    A race condition occurs when two or more threads access a shared resource, such as a global variable, simultaneously and the final result depends on the timing of the access. In a safety critical environment, where errors in the system can have severe consequences, race conditions can cause significant harm.

    // Example of a global variable, including a potential race condition
    int32_t g_temperature_C = 0;
    
    void thread1(void)
    {
      // Read the temperature from the sensor
      g_temperature_C = ReadTemperatureFromSensor();
    }
    
    void thread2(void)
    {
      if ((g_temperature_C > 0) && (g_temperature_C < 70)) // !! Simple race condition
      {
        // Do some safety critical work
      }
      else
      {
        // Manage temperature out of bounds (i.e., cool down or heat up)
      }
    }
    Code language: C++ (cpp)

    In the example above, thread2 is doing some safety critical work, but only when g_temperature_C is within a certain range, which is updated in thread1. If the temperature is out of bounds, then the system needs to take a different action. The issue here is that the wrong action can lead to serious consequences, either for the safety of the system, or in the case where humans are involved, the safety of the user.

    In this case, a global variable is a poor choice of scope for g_temperature_C.

    If you find you do have to use global variables, you can still limit their scope to the specific compilation unit where they are defined (i.e., the file where the variable is declared). You can do this by adding the static keyword to the variable declaration. The advantage to this is that it limits the scope of the variable to just the specific module, rather than the entire program.

    // Limit scope of global variable to the specific compilation unit (i.e., this file)
    static int32_t g_temperature_C = 0;
    Code language: C++ (cpp)

    Local Scope Variables

    Local scope variables, on the other hand, are declared within a function or block and are only accessible within that specific scope. They have a shorter lifetime, are allocated on the stack, and are automatically deallocated from memory once the function or block has finished execution. Using local scope variables is recommended over global variables as they limit the potential for naming conflicts, allow for cleaner code, and also eliminate race conditions.

    // Example of a local variable, resolving the race condition above
    void thread2(void)
    {
      int32_t l_temperature_C = ReadTemperatureFromSensor();
      if ((l_temperature_C > 0) && (l_temperature_C < 70)) // !! NO race condition
      {
        // Do some safety critical work
      }
      else
      {
        // Manage temperature out of bounds (i.e., cool down or heat up)
      }
    }
    Code language: C++ (cpp)

    As you can see, the race condition from using a global variable is avoided here because the variable is local and cannot be changed outside of this function.

    Member Scope Variables

    Member scope variables, also known as class member variables, are declared within a class and are accessible by all member functions of that class. Their scope is tied to the lifetime of the object they are a member of.

    #include <iostream>
    
    class TemperatureSensor
    {
    public:
      TemperatureSensor() = default;
    
      void GetTemperature()
      {
        m_temp_C = ReadTemperatureFromSensor();
        return m_temp_C;
      }
    
    private:
      int32_t m_temp_C{0};
    };
    
    int main()
    {
      TemperatureSensor sensor;
      std::cout << "Temperature: " << sensor.GetTemperature() << std::endl;
      std::cout << "Temperature: " << sensor.GetTemperature() << std::endl;
    
      return 0;
    }Code language: PHP (php)

    You can think of the scope of member variables to be similar to that of static global variables. Instead of being limited to the compilation unit where they are declared, they are limited to the scope of the class that they are part of. Race conditions on member variables are a real possibility. Precautions must be taken to ensure you avoid them, such as proper locking or an improved architecture to avoid the race altogether.

    Properly Scoping Pointers

    Pointers are a powerful tool in C and C++, allowing you to efficiently work with data objects in your programs. However, naive usage of pointers can lead to significant problems, including hard to find bugs and difficult to maintain code.

    In C and C++, pointers have their own lifetime, separate from the objects they point to. When a pointer goes out of scope, the object it referenced remains in memory but is no longer accessible. When dynamically allocating memory, this leads to memory leaks where the memory is not properly deallocated, leading to a buildup of memory usage over time.

    Smart Pointers

    To prevent memory leaks and ensure that your programs are efficient and reliable, it is important to handle pointers with care. Modern C++ provides smart pointers types, which automatically manage the lifetime of objects and deallocate them when they are no longer needed. Using smart pointer types of std::shared_ptr and std::unique_ptr, you can be assured that when you create (and allocate) a pointer to an object, that object is constructed (and initialized if following RAII principles) and the pointer is valid. Then, when that pointer goes out of scope, the object is destructed and the memory is deallocated.

    #include <memory>
    #include <iostream>
    
    void PrintTemperature()
    {
      // Create a unique pointer to a TemperatureSensor object
      std::unique_ptr<TemperatureSensor> pTS = std::make_unique<TemperatureSensor>();
      
      // Use the unique pointer within the scope of the current function
      std::cout << "Temperature: " << pTS->GetTemperature() << std::endl;
      
      // The unique pointer goes out of scope at the end of the main function
      // and its dynamically allocated memory is automatically deallocated
    
    }Code language: PHP (php)

    When working with raw pointers, it’s critical to be aware of the lifetime of the objects being pointed to. For example, if the lifetime of the object ends before the pointer is deallocated, the pointer becomes a “dangling pointer”. This can cause undefined behavior, such as crashing the program or returning incorrect results. Smart pointers are typically a better choice and avoid this risk by managing the lifetime of the object themselves.


    In conclusion, understanding and properly handling the scope of variables in C/C++ is a crucial aspect of writing clean, maintainable, and effective code. By becoming familiar with global, local, and member scopes, and considering the lifetime and accessibility of variables, you can make informed decisions about how to use variables in your programs.

    Additionally, pointers require their own set of considerations when it comes to scope, and it is essential to handle them with care to prevent memory leaks and other issues.

    By following best practices and being aware of the potential pitfalls, you can ensure that your programs are reliable, efficient, and easy to maintain.

  • Quick Tip: Python Variables in Multithreaded Applications and Why Your Choice Matters

    When working with Python classes, you have two kinds of variables you can work with: class variables and instance variables. We won’t get deep into the similarities and differences here, nor various use cases for each kind. For that, I’ll refer you to this article. For our purposes here, just know that class variables and instance variables both have their own unique uses.

    In this post, we’ll be looking at one particular use case: using Python class and instance variables in multithreaded applications. Why does this matter? As with any programming language, choice of variable types can be critical to the success or failure of your application.

    With that in mind, let’s explore some of the options you have when working with Python variables in a multithreaded context. Knowing which option to choose can make all the difference.

    Class variables are declared at the top of your class and can be accessed by every object that belongs to this particular class (i.e., each instance shares a copy of each class variable); while on the other hand, instance variables use the indicator called “self” and are tied to a specific instance. Each instance of an object will have its own set of instance variables, but share the class variables.

    Class variables look like this:

    class obj:
      data = [0,0,0]
    Code language: Python (python)

    Instance variables look like this:

    class obj:
      def __init__(self):
        self.data = [0,0,0]
    Code language: Python (python)

    I recently ran into an issue with class vs. instance variables in a multithreaded application. A colleague of mine was debugging a UI application that was communicating on an interface and shipping the received data off to a UI for display. He found it would crash randomly, so we switched up the architecture a bit to receive data on one thread and then pass it through a queue to the UI thread to consume. This seemed to resolve the crash, but the data in the UI was wrong.

    Digging into the problem, we found that the data was changing as it passed through the queue. After some more digging, my colleague realized that the class that was implemented to push the data through the queue was utilizing class variables instead of instance variables.

    This simple program illustrates the issue:

    import queue
    import threading
    import time
    
    q1 = queue.Queue()
    
    class obj:
      id = 0
      data = [0,0,0]
    
    def thread_fn(type):
        d = q1.get()
        preprocessing = d.data
        time.sleep(3)
    
        # Check data members post "processing", after modified by other thread
        if preprocessing != d.data:
          print(f"{type}: Before data: {preprocessing} != After data: {d.data}")
        else:
          print(f"{type}: Before data: {preprocessing} == After data: {d.data}")
    
    if __name__ == "__main__":
        x = threading.Thread(target=thread_fn, args=("ClassVars",))
        obj.id = 1
        obj.data = [1,2,3]
        q1.put(obj)
    
        x.start()
    
        # Update the data
        obj.id = 2
        obj.data = [4,5,6]
        q1.put(obj)
    
        x.join()
    Code language: Python (python)

    Essentially what was happening is that the data would be received on the interface (in this case the main function) and put into the queue. As the UI was getting around to processing said data, new data would be received on the interface and put it into the queue. Since class variables were used originally (and the class object was used directly), the old data got overwritten with the new data in the class and the UI would have the wrong data and generate errors during processing.

    Once the underlying message class was changed to use instance variables, the “bad data” issue went away and the original problem of the crashing application was also resolved with the architecture change. Take a look at the difference in this program:

    import queue
    import threading
    import time
    
    q1 = queue.Queue()
    
    class obj:
      def __init__(self):
        self.id = 0
        self.data = ['x','y','z']
    
    def thread_fn(type):
        d = q1.get()
        preprocessing = d.data
        time.sleep(3)
    
        # Check data members post "processing", after modified by other thread
        if preprocessing != d.data:
          print(f"{type}: Before data: {preprocessing} != After data: {d.data}")
        else:
          print(f"{type}: Before data: {preprocessing} == After data: {d.data}")
    
    if __name__ == "__main__":
        x = threading.Thread(target=thread_fn, args=("InstanceVars",))
        obj1 = obj()
        obj1.id = 1
        obj1.data = [1,2,3]
        q1.put(obj1)
    
        x.start()
    
        # Update the data
        obj2 = obj()
        obj2.id = 2
        obj2.data = [4,5,6]
        q1.put(obj2)
    
        x.join()
    Code language: Python (python)

    As you can see, using instance variables requires that we create an instance of each object to begin with. This ensures that each object created has its own data members that are independent of the other instances, which is exactly what we required in this scenario. This single change along would have likely cleaned up the issues we were seeing, but would not have fixed the root of the problem.

    When passing through the queue, the thread would get each instance and use the correct data for processing. Nothing in the thread function had to change; only how the data feeding it was set up.

    Python is a great language, but it definitely has its quirks. The next time you hit a snag while trying to parallelize your application, take a step back and understand the features of your programming language. Understanding the peculiarities of your chosen language is the mark of a mindful programmer! With a bit of space to gather your wits and some careful, conscious coding, you can avoid these pesky pitfalls and create fast, reliable threaded applications. What other Python nuances have bitten you in the past? Let us know in the comments below!