Tag: c/c++

  • 9 Tips for Writing Clean and Effective C/C++ Code

    Writing clean and effective code is essential for software developers. Not only does it make the code easier to maintain and update, but it also ensures that the code runs efficiently and without bugs. As a programming language, C/C++ is widely used in many applications, from system programming to game development. To help you write better C/C++ code, I’ve compiled a list of 10 tips from my laundry list of what makes good, clean, and effective C/C++ code. I hope these will guide you in making conscious decisions when coding, since many of these tips can be applied to other languages as well! So, whether you are an experienced C/C++ developer or just starting out, these tips will help you write cleaner, more efficient, and effective code.

    Tip #1: Variable Scope Awareness

    In C/C++, variables can have three different scopes: global scope, local scope, and member scope. Each of them have their place in software development and each have their own pros and cons.

    My rule of thumb is this. Make everything a local variable. If I need access to it in other object methods, I promote it to a member variable. If that still doesn’t work (which is extremely rare), then I make it a static global variable. With proper software design, I have found I never need to declare a true global variable, even if I protect it with appropriate locks.

    One last comment when dealing with global variables — you really should always make them const. The guidelines also state that you should always prefer scoped objects, rather than ones on the heap.

    Tip #2: Use Standard Types When Available

    Using standard type definitions in your C/C++ code has several benefits that can make your code more readable, portable, and maintainable. Here are some reasons why you should consider using standard type definitions in your code:

    1. Readability: Standard type definitions like size_t, int32_t, uint64_t, etc. are self-documenting and convey a clear meaning to the reader of your code. For example, using size_t instead of int to represent the size of a container makes it clear that the variable can only hold non-negative integers, which can help prevent bugs.
    2. Portability: Different platforms may have different data types with different sizes and behaviors. By using standard type definitions, you can ensure that your code is portable and will work consistently across different platforms.
    3. Type safety: Using standard type definitions can help prevent bugs caused by type mismatches, such as assigning a signed int to an unsigned int variable, or passing the wrong type of parameter as a function argument.
    4. Code maintenance: Standard type definitions can make your code easier to maintain by reducing the need for manual conversions and ensuring that the types of your variables are consistent throughout your codebase.

    Overall, using standard type definitions can help make your code more readable, portable, and maintainable, and following these recommendations can help you make conscious decisions about which type definitions to use in your code.

    Tip #3: Organize Related Data Into Objects

    When working with complex systems, it is often worthwhile to organize sets of data into objects for three primary reasons: encapsulation, abstraction, and modularity. Each of these are powerful principles that can help improve your code.

    Encapsulation

    Encapsulation is a fundamental principle of object-oriented programming and can help make your code more modular and maintainable.

    By organizing related data into an object, you can encapsulate the data and the operations that can be performed on it. This allows you to control access to the data and ensure that it is only modified in a safe and consistent way. In addition, you can make changes to the underlying data representation without changing the interface, which means that users of your object don’t have to change as well.

    Abstraction

    Objects allow you to abstract away the details of the data and provide a simplified interface for interacting with it. This can make your code easier to read and understand, as well as more resistant to changes in the underlying data representation.

    Modularity

    Organizing related data into an object can help you break down a large, complex problem into smaller, more manageable pieces. Each object can represent a distinct component of the system, with its own data and behavior, that can be developed and tested independently of the other components.

    Finally, once you have objects that you are manipulating, you can start returning those objects from your functions. Even cooler than that, you can return tuples containing your object and status information from your methods!

    Tip #4: Be Consistent in the Organization of Your Objects

    When you organize your data into objects and start defining member variables and methods, be consistent in the organization of your objects. For example, declare all public interface information up front, and keep all protected and private information at the end of the class.

    class BadExample
    {
    private:
      double m_data{73.0};
    
    public:
      BadExample();
      BadExample(const double &data) : m_data(data) {}
      ~BadExample();
    
      void SetFlag(const bool flag) { m_flag = flag; }
      void SetBytes(const std::size_t bytes) { m_nBytes = bytes; }
    
    private:
      bool m_flag{false};
      std::size_t m_nBytes{0};
    };
    
    class GoodExample
    {
    public:
      BadExample();
      BadExample(const double &data) : m_data(data) {}
      ~BadExample();
    
      void SetFlag(const bool flag) { m_flag = flag; }
      void SetBytes(const std::size_t bytes) { m_nBytes = bytes; }
    
    private:
      double m_data{73.0};
      bool m_flag{false};
      std::size_t m_nBytes{0};
    
      void SetData(const double data) { m_data = data; }
    };Code language: PHP (php)

    By declaring all private member variables and methods in a single private section, it makes the class definition much easier to read and follow. I know that when I read the GoodExample class definition that when I see the private keyword that everything coming after that keyword will be private and not accessible to me as a normal user.

    Tip #5: Place All Documentation in Header Files

    When you document your functions and variables, document them in the header file for one primary reason: keep the interface and implementation separate.

    Keeping the interface definition of your object separate from the implementation is a solid object-oriented design principle. The header file is where you define the interface for your users. That is where your users are going to look to understand what the purpose of a function is, how it should be used, what the arguments mean, and what the return value will contain. Many times the user of your object will not have access to the source code, so placing documentation there is pointless, from an interface perspective.

    Tip #6: Enforce a Coding Style

    Enforcing a code style can bring several benefits to your development process, including:

    1. Consistency: By enforcing a code style, you can ensure that your codebase looks consistent across different files and modules. This can make your code easier to read and understand, and can help reduce the amount of time developers spend trying to figure out how different parts of the codebase work.
    2. Maintainability: A consistent code style can also make your code easier to maintain, as it can help you identify patterns and common practices that are used throughout the codebase. This can make it easier to update and refactor the code, as you can more easily find and update all instances of a particular pattern.
    3. Collaboration: Enforcing a code style can also make it easier to collaborate with other developers, especially if they are working remotely or in different time zones. By using a consistent code style, developers can more easily understand each other’s code and can quickly identify where changes need to be made.
    4. Automation: Enforcing a code style with clang-format can also help automate the code review process, as it can automatically format code to the desired style. This can save time and effort in the code review process, and can ensure that all code is formatted consistently, even if developers have different preferences or habits.
    5. Industry standards: Many organizations and open-source projects have established code style guidelines that are enforced using tools like clang-format. By following these standards, you can ensure that your codebase adheres to best practices and can more easily integrate with other projects.

    Tip #7: Be const-Correct in All Your Definitions

    A major goal of mine when working in C and C++ is to make as many potential pitfalls and runtime bugs compiler errors rather than runtime errors. Striving to be const-correct in everything accomplishes a few things for the conscious coder:

    1. It conveys intent about what the method or variable should do or be. A const method cannot modify an object’s state, and a const variable cannot change its value post-declaration. This can make your code safer and reduce the risk of bugs and unexpected behavior.
    2. It makes your code more readable, as it can signal to other developers that the value of the object is not meant to be changed. This can make it easier for other developers to understand your code and can reduce confusion and errors.
    3. It allows the compiler to make certain optimizations that can improve the performance of your code. For example, the compiler can cache the value of a const object, which can save time in certain situations.
    4. It promotes a consistent coding style, making it easier for other developers to work with your code and reduce the risk of errors and confusion.
    5. It makes your code more compatible with other libraries and frameworks. Many third-party libraries require const-correctness in order to work correctly, so adhering to this standard can make it easier to integrate your code with other systems.

    Here are a couple of examples:

    class MyConstCorrectClass
    {
    public:
      MyConstCorrectClass() = default;
    
      void SetFlag(const bool flag) { m_flag = flag; } // Method not marked const because it modifies the state
                                                       // The argument is immutable though, and is thus marked const
      bool GetFlag() const { return m_flag' } // Marked as const because it does not modify state
    
    private:
      bool m_flag{false};
    };
    
    void function1(void)
    {
      MyConstCorrectClass A;
      A.SetFlag(true);
      std::cout << "A: " << A.GetFlag() << std::endl;
    
      const MyConstCorrectClass B;
      B.SetFlag(true);   // !! Compiler error because B is constant
      std::cout << "B: " << B.GetFlag() << std::endl;
    }Code language: PHP (php)

    Tip #8: Wrap Single-line Blocks With Braces

    Single-line blocks, such as those commonly found in if/else statements, should always be wrapped in braces. Beyond the arguments that it increases readability, maintainability, and consistency, for me this is a matter of safety. Consider this code:

    if (isSafe())
      setLED(LED::OFF);Code language: C++ (cpp)

    What happens when I need to take additional action when the function returns true? Sleeping developers would simply add the new action right after the setLED(LED::OFF) statement like this:

    if (isSafe())
      setLED(LED::OFF);
      controlLaser(LASER::ON, LASER::HIGH_POWER);
    Code language: C++ (cpp)

    Now consider the implications of such an action. The controlLaser(LASER::ON, LASER::HIGH_POWER); statement gets run every single time, not just if the function isSafe() returns true. This has serious consequences, which is exactly why you should always wrap your single-line blocks with braces!

    if (isSafe())
    {
      setLED(LED::OFF);
      controlLaser(LASER::ON, LASER::HIGH_POWER);
    }
    Code language: C++ (cpp)

    Tip #9: Keep Your Code Linear — Return from One Spot

    This is also known as the “single exit point” principle, but the core of it is that you want your code to be linear. Linear code is easier to read, to maintain, and debug. When you return from a function in multiple places, this can lead to hard to follow logic that obscures what the developer is really trying to accomplish. Consider this example:

    std::string Transaction::GetUUID(void) const
    {
      std::string uuid = xg::Guid();  // empty ctor for xg::Guid gives a nil UUID
      if (m_library->isActionInProgress())
      {
        return m_library->getActionIdInProgress();
      }
      return uuid;
    }
    Code language: C++ (cpp)

    This seems fairly simple to follow and understand, but it doesn’t follow the single exit point principle — the flow of the method is non-linear. If the logic in this function ever gets more complex, this can quickly get harder to debug. This simple change here ensures that the flow is linear and that future modifications follow suit.

    std::string Transaction::GetUUID(void) const
    {
      std::string uuid = xg::Guid();  // empty ctor for xg::Guid gives a nil UUID
      if (m_library->isActionInProgress())
      {
        uuid = m_library->getActionIdInProgress();
      }
      return uuid;
    }
    Code language: C++ (cpp)

    You may argue that the first function is slightly more efficient because you save the extra copy to the temporary variable uuid. But most any modern compiler worth using will optimize that copy out, and you’re left with the same performance in both.

    A quick bit of wisdom — simple code, even if it has more lines, more assignments, etc. is more often than not going to result in better performance than complex code. Why? The optimizer can more readily recognize simple constructs and optimize them than it can with complex algorithms that perform the same function!


    Conclusion

    In this post, we covered a variety of topics related to C++ programming best practices. We discussed the benefits of using standard type definitions, the importance of organizing related data into objects, the placement of function documentation comments, the use of clang-format to enforce code style, the significance of being const-correct in all your definitions, and the reasons why it is important to wrap single-line blocks with braces and to return from only a single spot in your function.

    By adhering to these best practices, C++ programmers can create code that is more readable, maintainable, and easy to debug. These principles help ensure that code is consistent and that common sources of errors, such as memory leaks or incorrect program behavior, are avoided.

    Overall, by following these best practices, C++ programmers can create high-quality, efficient, and robust code that can be easily understood and modified, even as the codebase grows in size and complexity.

  • 4 Must-Have C++17 Features for Embedded Developers

    C++17 is a version of the C++ programming language that was standardized in 2017, and adds additonal new features and improvements to what is considered “modern C++”. Some major new features that I have really loved in C++17 include:

    • Structured bindings: Allows you to easily extract multiple variables from a single tuple or struct, and to use them in a more readable way.
    • Inline variables: Allows for the definition of variables with the “inline” keyword in classes and elsewhere.
    • New attributes: Allows for more readable code by marking certain areas with specific attributes that the compiler understands.
    • std::shared_mutex: Allows for multiple “shared” locks and a single “exclusive” lock. This is basically a standard read/write mutex!

    In embedded systems, support for C++17 will depend on the compiler and platform you are using. Some more popular compilers, such as GCC and Clang, have already added support for C++17. However, support for C++17 for your project may be limited due to the lack of resources and the need to maintain backwards compatibility with older systems.

    A good summary of the new features and improvements to the language can be found on cppreference.com. They also provide a nice table showing compiler support for various compilers.

    Structured Bindings

    I already wrote about my love for structured bindings in another post (and another as well), but this article would be incomplete without listing this feature!

    My main use of structured bindings is to extract named variables from a std::tuple, whether that be a tuple of my own creation or one returned to me by a function.

    But I just realized there is another use for them that makes things so much better when iterating over std::maps:

    // What I used to write
    for (const auto &val : myMap)
    {
        // Print out map data, key:value
        std::cout << val.first << ':' << val.second << std::endl;
    }
    
    // What I now write
    for (const auto &[key, value] : myMap)
    {
        // Print out map data, key:value
        std::cout << key << ':' << value << std::endl;
    }Code language: PHP (php)

    The second for loop is much more readable and much easier for a maintainer to understand what is going on. Conscious coding at its best!

    Inline Variables

    An inline variable is a variable that is defined in the header file and is guaranteed to be the same across all translation units that include the header file. This means that each compilation unit will have its own copy of the variable, as opposed to a single shared copy like a normal global variable.

    One of the main benefits of inline variables is that they allow for better control over the storage duration and linkage of variables, which can be useful for creating more flexible and maintainable code.

    I find this new feature most useful when declaring static variables inside my class. Now I can simply declare a static class variable like this:

    class MyClass
    {
      public:
        static const inline std::string MyName = "MyPiClass";
        static constexpr inline double MYPI = 3.14159265359;
        static constexpr double TWOPI = 2*MYPI; // note that inline is not required here because constexpr implies inline
    };Code language: PHP (php)

    This becomes especially useful when you would like to keep a library to a single header, and you can avoid hacks that complicate the code and make it less readable.

    New C++ Attributes

    C++17 introduces a few standard attributes that make annotating your code for the compiler much nicer. These attributes are as follows.

    [[fallthrough]]

    This attribute is used to allow case body to fallthrough to the next without compiler warnings.

    In the example given by C++ Core Guidelines ES.78, having a case body fallthrough to the next case leads to hard to find bugs, and it just is plain hard to read. However, there are certain instances where this is absolutely appropriate. In those cases, you simply add the [[fallthough]]; attribute where the break statement would normally go.

    switch (eventType) {
    case Information:
        update_status_bar();
        break;
    case Warning:
        write_event_log();
        [[fallthough]];
    case Error:
        display_error_window();
        break;
    }Code language: JavaScript (javascript)

    [[maybe_unused]]

    This attribute is used to mark entities that might be unused, to prevent compiler warnings.

    Normally, when writing functions that take arguments that the body does not use, the approach has been to cast those arguments to void to eliminate the warning. Many static analyzers actually suggest this as the suggestion. The Core Guidelines suggest to simply not provide a name for those arguments, which is the preferred approach.

    However, in the cases where the argument is conditionally used, you can mark the argument with the [[maybe_unused]] attribute. This communicates to the maintainer that the argument is not used in all cases, but is still required.

    RPC::Status RPCServer::HandleStatusRequest(
        const RPC::StatusRequest &r [[maybe_unused]])
    {
      if (!m_ready)
      {
        return RPC::Status();
      }
      return ProcessStatus(r);
    }Code language: PHP (php)

    This attribute can also be used to mark a static function as possible unused, such as if it is conditionally used based on whether DEBUG builds are enabled.

    [[maybe_unused]] static std::string toString(const ProcessState state);Code language: PHP (php)

    [[nodiscard]]

    This attribute is extremely useful when writing robust and reliable library code. When marking a function or method with this attribute, the compiler will generate errors if a return value is not used.

    Many times, developers will discard return values by casting the function to void, like this.

    (void)printf("Testing...");Code language: JavaScript (javascript)

    This is against the Core Guidelines ES-48, but how do you get the compiler to generate errors for your functions in a portable, standard way? With [[nodiscard]]. When a developer fails to check the return value of your function (i.e., they don’t store the result in some variable or use it in a conditional), the compiler will tell them there is a problem.

    // This code will generate a compiler error
    [[nodiscard]] bool checkError(void)
    {
      return true;
    }
    
    int main(void)
    {
      checkError();
      return 0;
    }
    
    // Error generated
    scratch.cpp:36:13: error: ignoring return value of ‘bool checkError()’, declared with attribute ‘nodiscard’ [-Werror=unused-result]
       36 |   checkError();
    
    
    // However, this takes care of the issue because we utilize the return value
    if (checkError())
    {
      std::cout << "An error occurred!" << std::endl;
    }Code language: JavaScript (javascript)

    I love how this can be used to make your users think about what they are doing!

    Shared (Read/Write) Mutex

    A shared lock allows multiple threads to simultaneously read a shared resource, while an exclusive lock allows a single thread to modify the resource.

    In many instances, it is desirable to have a lock that protects readers from reading stale data. That is the whole purpose of a mutex. However, with a standard mutex, if one reader holds the lock, then additional readers have to wait for the lock to be released. When you have many readers trying to acquire the same lock, this can result in unnecessarily long wait times.

    With a shared lock (or a read/write mutex), the concept of a read lock and a write lock are introduced. A reader must wait for the write lock to be released, but will simply increment the read lock counter when taking the lock. A writer, on the other hand, must wait for all read and write locks to be released before it can acquire a write lock. Essentially, readers acquire and hold a shared lock, while writers acquire and hold an exclusive lock.

    Here is an example of how to use a shared_mutex:

    #include <iostream>
    #include <thread>
    #include <mutex>
    
    std::shared_mutex mtx;
    int sharedCount = 0;
    
    void writevalue()
    {
        for (int i = 0; i < 10000; ++i)
        {
            // Get an exclusive (write) lock on the shared_mutex
            std::unique_lock<std::shared_mutex> lock(mtx);
            ++sharedCount ;
        }
    }
    
    void read()
    {
        for (int i = 0; i < 10000; ++i)
        {
            // Get a shared (read) lock on the shared_mutex
            std::shared_lock<std::shared_mutex> lock(mtx);
            std::cout << sharedCount << std::endl;
        }
    }
    
    int main()
    {
        std::thread t1(writevalue);
        std::thread t2(read);
        std::thread t3(read);
    
        t1.join();
        t2.join();
        t3.join();
    
        std::cout << sharedCount << std::endl;
        return 0;
    }
    
    Code language: C++ (cpp)

    Here you can see that std::shared_mutex is used to protect the shared resource sharedCount. Thread t1 increments the counter using an exclusive lock, while threads t2 and t3 read the counter using shared locks. This allows for concurrent read operations and exclusive write operation. It greatly improves performance where you have a high number of read operations versus write operations.

    With C++17, this type of lock is standardized and part of the language. I have found this to be extremely useful and makes my code that much more portable when I need to use this type of mutex!


    C++17 offers a wide range of new features that provide significant improvements to the C++ programming language. The addition of structured bindings, inline variables, and the new attributes make code more readable and easier to maintain, and the introduction of the “std::shared_mutex” type provides performance improvements in the situations where that type of lock makes sense. Overall, C++17 provides an even more modern and efficient programming experience for developers. I encourage you to start exploring and utilizing these new features in your own projects!