Fluent C Principles, Practices, and Patterns (Christopher Preschern) (Z-Library)

Author: Christopher Preschern

科学

Expert advice on C programming is hard to find. While much help is available for object-oriented programming languages, there's surprisingly little for the C language. With this hands-on guide, beginners and experienced C programmers alike will find guidance about design decisions, including how to apply them bit by bit to running code examples when building large-scale programs. Christopher Preschern, a leading member of the design patterns community, answers questions such as how to structure C programs, cope with error handling, or design flexible interfaces. Whether you're looking for one particular pattern or an overview of design options for a specific topic, this book shows you how to implement hands-on design knowledge specifically for the C programming language. You'll find design patterns for: • Error handling • Returning error information • Memory management • Returning data from C functions • Data lifetime and ownership • Flexible APIs • Flexible iterator interfaces • Organizing files in modular programs • Escaping #ifdef Hell Tags

📄 File Format: PDF
💾 File Size: 7.3 MB
16
Views
0
Downloads
0.00
Total Donations

📄 Text Preview (First 20 pages)

ℹ️

Registered users can read the full content for free

Register as a Gaohf Library member to read the complete e-book online for free and enjoy a better reading experience.

📄 Page 1
Christopher Preschern Fluent C Principles, Practices, and Patterns Preschern Fluent C Fluent C
📄 Page 2
C / PROGR AMMING L ANGUAGES “Full of real-world examples, Fluent C is an excellent resource for making your code cleaner and more maintainable.” —David Griffiths Author of Head First C ”This book guides novices toward making industrial-strength products in C.” —Robert Hanmer Software Architect Fluent C US $59.99 CAN $74.99 ISBN: 978-1-492-109733-4 Twitter: @oreillymedia linkedin.com/company/oreilly-media youtube.com/oreillymedia Expert advice on C programming is hard to find. While much help is available for object-oriented programming languages, there’s surprisingly little for the C language. With this hands- on guide, beginners and experienced C programmers alike will find guidance about design decisions, including how to apply them bit by bit to running code examples when building large-scale programs. Christopher Preschern, a leading member of the design patterns community, answers questions about how to structure C programs, cope with error handling, and design flexible interfaces. Whether you’re looking for one particular pattern or an overview of design options for a specific topic, this book shows you the way. In Part I, you’ll learn how to implement hands-on design knowledge specifically for the C programming language. Part II includes stories of applying C programming patterns to implement larger programs. You’ll learn how patterns can make your life easier through good design decisions. This book includes patterns for: • Error handling • Returning error information • Memory management • Returning data from C functions • Data lifetime and ownership • Flexible APIs • Flexible iterator interfaces • Organizing files in modular programs • Escaping #ifdef hell Christopher Preschern organizes design pattern conferences and initiatives to improve pattern writing. As a C programmer at the company ABB, he gathered and documented hands-on knowledge on how to write industrial-strength code. He has lectured on coding and quality at Graz University of Technology and holds a PhD in computer science. Preschern Fluent C Fluent C
📄 Page 3
Christopher Preschern Fluent C Principles, Practices, and Patterns Boston Farnham Sebastopol TokyoBeijing
📄 Page 4
978-1-492-09733-4 [LSI] Fluent C by Christopher Preschern Copyright © 2023 Christopher Preschern. All rights reserved. Printed in the United States of America. Published by O’Reilly Media, Inc., 1005 Gravenstein Highway North, Sebastopol, CA 95472. O’Reilly books may be purchased for educational, business, or sales promotional use. Online editions are also available for most titles (https://oreilly.com). For more information, contact our corporate/institu‐ tional sales department: 800-998-9938 or corporate@oreilly.com. Acquisitions Editor: Brian Guerin Development Editor: Corbin Collins Production Editor: Jonathon Owen Copyeditor: Piper Editorial Consulting, LLC Proofreader: Justin Billing Indexer: Judith McConville Interior Designer: David Futato Cover Designer: Karen Montgomery Illustrator: Kate Dullea October 2022: First Edition Revision History for the First Edition 2022-10-14: First Release See https://oreilly.com/catalog/errata.csp?isbn=9781492097334 for release details. The O’Reilly logo is a registered trademark of O’Reilly Media, Inc. Fluent C, the cover image, and related trade dress are trademarks of O’Reilly Media, Inc. The views expressed in this work are those of the author and do not represent the publisher’s views. While the publisher and the author have used good faith efforts to ensure that the information and instructions contained in this work are accurate, the publisher and the author disclaim all responsibility for errors or omissions, including without limitation responsibility for damages resulting from the use of or reliance on this work. Use of the information and instructions contained in this work is at your own risk. If any code samples or other technology this work contains or describes is subject to open source licenses or the intellectual property rights of others, it is your responsibility to ensure that your use thereof complies with such licenses and/or rights.
📄 Page 5
Table of Contents Preface. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . vii Part I. C Patterns 1. Error Handling. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 Running Example 5 Function Split 6 Guard Clause 9 Samurai Principle 12 Goto Error Handling 16 Cleanup Record 19 Object-Based Error Handling 22 Summary 26 Further Reading 27 Outlook 27 2. Returning Error Information. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 Running Example 30 Return Status Codes 32 Return Relevant Errors 39 Special Return Values 45 Log Errors 48 Summary 55 Further Reading 55 Outlook 56 iii
📄 Page 6
3. Memory Management. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57 Data Storage and Problems with Dynamic Memory 59 Running Example 61 Stack First 62 Eternal Memory 65 Lazy Cleanup 69 Dedicated Ownership 72 Allocation Wrapper 76 Pointer Check 81 Memory Pool 84 Summary 90 Further Reading 90 Outlook 91 4. Returning Data from C Functions. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93 Running Example 94 Return Value 95 Out-Parameters 99 Aggregate Instance 103 Immutable Instance 108 Caller-Owned Buffer 111 Callee Allocates 116 Summary 120 Outlook 120 5. Data Lifetime and Ownership. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121 Stateless Software-Module 123 Software-Module with Global State 127 Caller-Owned Instance 132 Shared Instance 138 Summary 144 Further Reading 145 Outlook 145 6. Flexible APIs. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147 Header Files 149 Handle 152 Dynamic Interface 156 Function Control 159 Summary 163 Further Reading 163 Outlook 164 iv | Table of Contents
📄 Page 7
7. Flexible Iterator Interfaces. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165 Running Example 167 Index Access 168 Cursor Iterator 172 Callback Iterator 177 Summary 182 Further Reading 183 Outlook 183 8. Organizing Files in Modular Programs. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185 Running Example 187 Include Guard 189 Software-Module Directories 192 Global Include Directory 197 Self-Contained Component 201 API Copy 207 Summary 216 Outlook 216 9. Escaping #ifdef Hell. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 217 Running Example 219 Avoid Variants 220 Isolated Primitives 224 Atomic Primitives 227 Abstraction Layer 231 Split Variant Implementations 236 Summary 242 Further Reading 243 Outlook 243 Part II. Pattern Stories 10. Implementing Logging Functionality. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 247 The Pattern Story 247 File Organization 248 Central Logging Function 249 Logging Source Filter 250 Conditional Logging 252 Multiple Logging Destinations 253 File Logging 254 Cross-Platform Files 255 Table of Contents | v
📄 Page 8
Using the Logger 259 Summary 259 11. Building a User Management System. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 261 The Pattern Story 261 Data Organization 261 File Organization 263 Authentication: Error Handling 264 Authentication: Error Logging 266 Adding Users: Error Handling 267 Iterating 269 Using the User Management System 272 Summary 272 12. Conclusion. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 275 What You’ve Learned 275 Further Reading 275 Closing Remarks 276 Index. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 277 vi | Table of Contents
📄 Page 9
Preface You picked up this book to move your programming skills one step forward. That is good, because you’ll definitely benefit from the hands-on knowledge provided in this book. If you have a lot of experience programming in C, you’ll learn the details of good design decisions and about their benefits and drawbacks. If you are fairly new to C programming, you’ll find guidance about design decisions, and you’ll see how these decisions are applied bit by bit to running code examples for building larger scale programs. The book answers questions such as how to structure a C program, how to cope with error handling, or how to design flexible interfaces. As you learn more about C pro‐ gramming, questions often pop up, such as the following: • Should I return any error information I have? • Should I use the global variable errno to do that? • Should I have few functions with many parameters or the other way around? • How do I build a flexible interface? • How can I build basic things like an iterator? For object-oriented languages, most of these questions are answered to a great extent by the Gang of Four book Design Patterns: Elements of Reusable Object-Oriented Soft‐ ware by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides (Prentice Hall, 1997). Design patterns provide a programmer with best practices on how objects should interact and which object owns which other kinds of objects. Also, design patterns show how such objects can be grouped together. However, for procedural programming languages like C, most of these design pat‐ terns cannot be implemented in the way described by the Gang of Four. There are no native object-oriented mechanisms in C. It is possible to emulate inheritance or poly‐ morphism in the C programming language, but that might not be the first choice, because such emulation makes things unfamiliar for programmers who are used to vii
📄 Page 10
programming C and are not used to programming with object-oriented languages like C++ and using concepts like inheritance and polymorphism. Such programmers may want to stick to their native C programming style that they are used to. However, with the native C programming style, not all object-oriented design patterns guidance is usable, or at least the specific implementation of the idea presented in a design pat‐ tern is not provided for non-object-oriented programming languages. And that is where we stand: we want to program in C, but we cannot directly use most of the knowledge documented in design patterns. This book shows how to bridge this gap and implement hands-on design knowledge for the C programming language. Why I Wrote This Book Let me tell you why the knowledge gathered in this book turned out to be very important for me and why such knowledge is hard to find. In school I learned C programming as my first programming language. Just like every new C programmer, I wondered why arrays start with index 0, and I first rather ran‐ domly tried out how to place the operators * and & in order to finally get the C pointer magic working. At university I learned how C syntax actually works and how it translates to bits and bytes on the hardware. With that knowledge I was able to write small programs that worked very well. However, I still had trouble understanding why longer code looked the way it did, and I certainly wouldn’t have come up with solutions like the following: typedef struct INTERNAL_DRIVER_STRUCT* DRIVER_HANDLE; typedef void (*DriverSend_FP)(char byte); typedef char (*DriverReceive_FP)(); typedef void (*DriverIOCTL_FP)(int ioctl, void* context); struct DriverFunctions { DriverSend_FP fpSend; DriverReceive_FP fpReceive; DriverIOCTL_FP fpIOCTL; }; DRIVER_HANDLE driverCreate(void* initArg, struct DriverFunctions f); void driverDestroy(DRIVER_HANDLE h); void sendByte(DRIVER_HANDLE h, char byte); char receiveByte(DRIVER_HANDLE h); void driverIOCTL(DRIVER_HANDLE h, int ioctl, void* context); viii | Preface
📄 Page 11
Looking at code like that prompted many questions: • Why have function pointers in the struct? • Why do the functions need that DRIVER_HANDLE? • What is an IOCTL, and why would I not have separate functions instead? • Why have explicit create and destroy functions? These questions came up as I began writing industrial applications. I regularly came across situations where I realized I did not have the C programming knowledge, for example, to decide how to implement an iterator or to decide how to cope with error handling in my functions. I realized that although I knew C syntax, I had no clue how to apply it. I tried to achieve something but just managed to do it in a clumsy way or not at all. What I needed were best practices on how to achieve specific tasks with the C programming language. For example, I needed to know things like the following: • How can I acquire and release resources in an easy way? • Is it a good idea to use goto for error handling? • Should I design my interface to be flexible, or should I simply change it when the need arises? • Should I use an assert statement, or should I return an error code? • How is an iterator implemented in C? It was very interesting for me to realize that while my experienced work colleagues had many different answers for these questions, nobody could point me to anything that documented these design decisions and their benefits and drawbacks. So next I turned to the internet, and yet again I was surprised: it was very hard to find sound answers to these questions even though the C programming language has been around for decades. I found out that while there is much literature on the C program‐ ming language basics and its syntax, there’s not much on advanced C programming topics or how to write beautiful C code that holds up to industrial applications. And that is exactly where this book comes in. This book teaches you how to advance your programming skills from writing basic C programs to writing larger-scale C programs that consider error handling and that are flexible regarding certain future changes in requirements and design. This book uses the concept of design patterns to provide you bit by bit with design decisions and their benefits and drawbacks. These design patterns are applied to running code examples that teach you how code like the earlier example evolves and why it ends up looking the way it does. Preface | ix
📄 Page 12
The presented patterns can be applied to any C programming domains. As I come from the domain of embedded programming in a multithreaded real-time environ‐ ment, some of the patterns are biased towards that domain. Anyways, you’ll see that the general idea of the patterns can be applied to other C programming domains and even beyond the scope of C programming. Patterns Basics The design guidance in this book is provided in the form of patterns. The idea of pre‐ senting knowledge and best practices in the form of patterns comes from the archi‐ tect Christopher Alexander in The Timeless Way of Building (Oxford University Press, 1979). He uses small pieces of well-proven solutions to tackle a huge problem in his domain: how to design and construct cities. The approach of applying patterns was adopted by the software development domain, where pattern conferences like the conference on Pattern Languages of Programs (PLoP) are held to extend the body of knowledge of patterns. In particular, the book Design Patterns: Elements of Reusable Object-Oriented Software by the Gang of Four (Prentice Hall, 1997) had a significant impact and made the concept of design patterns well known to software developers. But what exactly is a pattern? There are many definitions out there, and if you are deeply interested in the topic, then the book Pattern-Oriented Software Architecture: On Patterns and Pattern Languages by Frank Buschmann et al. (Wiley, 2007) can pro‐ vide you with accurate descriptions and details. For the purposes of this book, a pat‐ tern provides a well-proven solution to a real-life problem. The patterns presented in this book have the structure shown in Table P-1. Table P-1. How patterns are broken down in this book Pattern section Description Name This is the name of the pattern, which should be easy to remember. The aim is that this name will be used by programmers in their everyday language (as is the case with the Gang of Four patterns, where you hear programmers say, “And the Abstract Factory creates the object”). Pattern names are capitalized in this book. Context The context section sets the scene for the pattern. It tells you under which circumstances this pattern can be applied. Problem The problem section gives you information about the issue you want to tackle. It starts with the major problem statement written in bold font type and then adds details on why the problem is hard to solve. (In other pattern formats, these details go into a separate section called “forces.”) Solution This section provides guidance on how to tackle the problem. It starts with stating the main idea of the solution written in bold font type and continues with details about the solution. It also provides a code example in order to give very concrete guidance. Consequences This section lists the benefits and drawbacks of applying the described solution. When applying a pattern, you should always confirm that the consequences that arise are OK with you. Known uses The known uses give you evidence that the proposed solution is good and actually works in real-life applications. They also show you concrete examples to help you understand how to apply the pattern. x | Preface
📄 Page 13
A major benefit of presenting design guidance in the form of patterns is that these patterns can be applied one after another. If you have a huge design problem, it’s hard to find the one guidance document and the one solution that addresses exactly that problem. Instead, you can think of your huge and very specific problem as a sum of many smaller and more generic problems, and you can tackle these problems bit by bit by applying one pattern after the other. You simply check the problem descrip‐ tions of the patterns and apply the one that fits your problem and that has conse‐ quences you can live with. These consequences might lead to another problem that you can then address by applying another pattern. That way you incrementally design your code instead of trying to come up with a complete up-front design before even writing the first line of code. How to Read This Book You should already know C programming basics. You should know the C syntax and how it works—for example, this book won’t teach you what a pointer is or how to use it. This book delivers hints and guidance on advanced topics. The chapters in this book are self-standing. You can read them in an arbitrary order, and you can simply pick out the topics you are interested in. You’ll find an overview of all patterns in the next section, and from there you can jump to the patterns you are interested in. So if you know exactly what you are looking for, you can start right there. If you are not looking for one particular pattern, but instead want to get an overview of possible C design options, read through Part I of the book. Each chapter there focuses on a particular topic, starting with basic topics like error handling and mem‐ ory managment, and then moving to more advanced and specific topics like interface design or platform-independent code. The chapters each present patterns related to that topic and a running code example that shows bit by bit how the patterns can be applied. Part II of this book shows two larger running examples that apply many of the pat‐ terns from Part I. Here you can learn how to build up some larger piece of software bit by bit through the application of patterns. Overview of the Patterns You’ll find an overview of all patterns presented in this book in Tables P-2 through P-10. The tables show a short form of the patterns that only contains a brief descrip‐ tion of the core problem, followed by the keyword “Therefore,” followed by the core solution. Preface | xi
📄 Page 14
Table P-2. Patterns for error handling Pattern name Summary “Function Split” on page 6 The function has several responsibilities, which makes the function hard to read and maintain. Therefore, split it up. Take a part of a function that seems useful on its own, create a new function with that, and call that function. “Guard Clause” on page 9 The function is hard to read and maintain because it mixes pre-condition checks with the main program logic of the function. Therefore, check if you have mandatory pre-conditions, and immediately return from the function if these pre-conditions are not met. “Samurai Principle” on page 12 When returning error information, you assume that the caller checks for this information. However, the caller can simply omit this check and the error might go unnoticed. Therefore, return from a function victorious or not at all. If there is a situation for which you know that an error cannot be handled, then abort the program. “Goto Error Handling” on page 16 Code gets difficult to read and maintain if it acquires and cleans up multiple resources at different places within a function. Therefore, have all resource cleanup and error handling at the end of the function. If a resource cannot be acquired, use the goto statement to jump to the resource cleanup code. “Cleanup Record” on page 19 It is difficult to make a piece of code easy to read and maintain if this code acquires and cleans up multiple resources, particularly if those resources depend on one another. Therefore, call resource acquisition functions as long as they succeed, and store which functions require cleanup. Call the cleanup functions depending on these stored values. “Object-Based Error Handling” on page 22 Having multiple responsibilities in one function, such as resource acquisition, resource cleanup, and usage of that resource, makes that code difficult to implement, read, maintain, and test. Therefore, put initialization and cleanup into separate functions, similar to the concept of constructors and destructors in object-oriented programming. Table P-3. Patterns for returning error information Pattern name Summary “Return Status Codes” on page 32 You want to have a mechanism to return status information to the caller, so that the caller can react to it. You want the mechanism to be simple to use, and the caller should be able to clearly distinguish between different error situations that could occur. Therefore, use the Return Value of a function to return status information. Return a value that represents a specific status. Both of you as the callee and the caller must have a mutual understanding of what the value means. “Return Relevant Errors” on page 39 On the one hand, the caller should be able to react to errors; on the other hand, the more error information you return, the more your code and the code of your caller have to deal with error handling, which makes the code longer. Longer code is harder to read and maintain and brings in the risk of additional bugs. Therefore, only return error information to the caller if that information is relevant to the caller. Error information is only relevant to the caller if the caller can react to that information. “Special Return Values” on page 45 You want to return error information, but it’s not an option to explicitly Return Status Codes because that implies that you cannot use the Return Value of the function to return other data. You’d have to return that data via Out-Parameters, which would make calling your function more difficult. Therefore, use the Return Value of your function to return the data computed by the function. Reserve one or more special values to be returned if an error occurs. “Log Errors” on page 48 You want to make sure that in case of an error you can easily find out its cause. However, you don’t want your error-handling code to become complicated because of this. Therefore, use different channels to provide error information that is relevant for the calling code and error information that is relevant for the developer. For example, write debug error information into a log file and don’t return the detailed debug error information to the caller. xii | Preface
📄 Page 15
Table P-4. Patterns for memory management Pattern name Summary “Stack First” on page 62 Deciding the storage class and memory section (stack, heap, …) for variables is a decision every programmer has to make often. It gets exhausting if for each and every variable, the pros and cons of all possible alternatives have to be considered in detail. Therefore, simply put your variables on the stack by default to profit from automatic cleanup of stack variables. “Eternal Memory” on page 65 Holding large amounts of data and transporting it between function calls is difficult because you have to make sure that the memory for the data is large enough and that the lifetime extends across your function calls. Therefore, put your data into memory that is available throughout the whole lifetime of your program. “Lazy Cleanup” on page 69 Having dynamic memory is required if you need large amounts of memory and memory where you don’t know the required size beforehand. However, handling cleanup of dynamic memory is a hassle and is the source of many programming errors. Therefore, allocate dynamic memory and let the operating system cope with deallocation by the end of your program. “Dedicated Ownership” on page 72 The great power of using dynamic memory comes with the great responsibility of having to properly clean that memory up. In larger programs, it becomes difficult to make sure that all dynamic memory is cleaned up properly. Therefore, right at the time when you implement memory allocation, clearly define and document where it’s going to be cleaned up and who is going to do that. “Allocation Wrapper” on page 76 Each allocation of dynamic memory might fail, so you should check allocations in your code to react accordingly. This is cumbersome because you have many places for such checks in your code. Therefore, wrap the allocation and deallocation calls, and implement error handling or additional memory management organization in these wrapper functions. “Pointer Check” on page 81 Programming errors that lead to accessing an invalid pointer cause uncontrolled program behavior, and such errors are difficult to debug. However, because your code works with pointers frequently, there is a good chance that you have introduced such programming errors. Therefore, explicitly invalidate uninitialized or freed pointers and always check pointers for validity before accessing them. “Memory Pool” on page 84 Frequently allocating and deallocating objects from the heap leads to memory fragmentation. Therefore, hold a large piece of memory throughout the whole lifetime of your program. At runtime, retrieve fixed-size chunks of that memory pool instead of directly allocating new memory from the heap. Table P-5. Patterns for returning data from C functions Pattern name Summary “Return Value” on page 95 The function parts you want to split are not independent from one another. As usual in procedural programming, some part delivers a result that is then needed by some other part. The function parts that you want to split need to share some data. Therefore, simply use the one C mechanism intended to retrieve information about the result of a function call: the Return Value. The mechanism to return data in C copies the function result and provides the caller access to this copy. “Out-Parameters” on page 99 C only supports returning a single type from a function call, and that makes it complicated to return multiple pieces of information. Therefore, return all the data with a single function call by emulating by- reference arguments with pointers. “Aggregate Instance” on page 103 C only supports returning a single type from a function call, and that makes it complicated to return multiple pieces of information. Therefore, put all data that is related into a newly defined type. Define this Aggregate Instance to contain all the related data that you want to share. Define it in the interface of your component to let the caller directly access all the data stored in the instance. Preface | xiii
📄 Page 16
Pattern name Summary “Immutable Instance” on page 108 You want to provide information held in large pieces of immutable data from your component to a caller. Therefore, have an instance (for example, a struct) containing the data to share in static memory. Provide this data to users who want to access it and make sure that they cannot modify it. “Caller-Owned Buffer” on page 111 You want to provide complex or large data of known size to the caller, and that data is not immutable (it changes at runtime). Therefore, require the caller to provide a buffer and its size to the function that returns the large, complex data. In the function implementation, copy the required data into the buffer if the buffer size is large enough. “Callee Allocates” on page 116 You want to provide complex or large data of unknown size to the caller, and that data is not immutable (it changes at runtime). Therefore, allocate a buffer with the required size inside the function that provides the large, complex data. Copy the required data into the buffer and return a pointer to that buffer. Table P-6. Patterns for data lifetime and ownership Pattern name Summary “Stateless Software-Module” on page 123 You want to provide logically related functionality to your caller and make that functionality as easy as possible for the caller to use. Therefore, keep your functions simple and don’t build up state information in your implementation. Put all related functions into one header file and provide the caller this interface to your software-module. “Software-Module with Global State” on page 127 You want to structure your logically related code that requires common state information and make that functionality as easy as possible for the caller to use. Therefore, have one global instance to let your related functions share common resources. Put all functions that operate on this instance into one header file, and provide the caller this interface to your software-module. “Caller-Owned Instance” on page 132 You want to provide multiple callers or threads access to functionality with functions that depend on one another, and the interaction of the caller with your functions builds up state information. Therefore, require the caller to pass an instance, which is used to store resource and state information, along to your functions. Provide explicit functions to create and destroy these instances, so that the caller can determine their lifetime. “Shared Instance” on page 138 You want to provide multiple callers or threads access to functionality with functions that depend on one another, and the interaction of the caller with your functions builds up state information, which your callers want to share. Therefore, require the caller to pass an instance, which is used to store resource and state information, along to your functions. Use the same instance for multiple callers and keep the ownership of that instance in your software-module. Table P-7. Patterns for flexible APIs Pattern name Summary “Header Files” on page 149 You want functionality that you implement to be accessible to code from other implementation files, but you want to hide your implementation details from the caller. Therefore, provide function declarations in your API for any functionality you want to provide to your user. Hide any internal functions, internal data, and your function definitions (the implementations) in your implementation file and don’t provide this implementation file to the user. “Handle” on page 152 You have to share state information or operate on shared resources in your function implementations, but you don’t want your caller to see or even access all that state information and shared resources. Therefore, have a function to create the context on which the caller operates and return an abstract pointer to internal data for that context. Require the caller to pass that pointer to all your functions, which can then use the internal data to store state information and resources. xiv | Preface
📄 Page 17
Pattern name Summary “Dynamic Interface” on page 156 It should be possible to call implementations with slightly deviating behaviors, but it should not be necessary to duplicate any code, not even the control logic implementation and interface declaration. Therefore, define a common interface for the deviating functionalities in your API and require the caller to provide a callback function for that functionality, which you then call in your function implementation. “Function Control” on page 159 You want to call implementations with slightly deviating behaviors, but you don’t want to duplicate any code, not even the control logic implementation or the interface declaration. Therefore, add a parameter to your function that passes meta-information about the function call and that specifies the actual functionality to be performed. Table P-8. Patterns for flexible iterator interfaces Pattern name Summary “Index Access” on page 168 You want to make it possible for the user to iterate elements in your data structure in a convenient way, and it should be possible to change internals of the data structure without resulting in changes to the user’s code. Therefore, provide a function that takes an index to address the element in your underlying data structure and return the content of this element. The user calls this function in a loop to iterate over all elements. “Cursor Iterator” on page 172 You want to provide an iteration interface to your user which is robust in case the elements change during the iteration and which enables you to change the underlying data structure at a later point without requiring any changes to the user’s code. Therefore, create an iterator instance that points to an element in the underlying data structure. An iteration function takes this iterator instance as argument, retrieves the element the iterator currently points to, and modifies the iteration instance to point to the next element. The user then iteratively calls this function to retrieve one element at a time. “Callback Iterator” on page 177 You want to provide a robust iteration interface which does not require the user to implement a loop in the code for iterating over all elements and which enables you to change the underlying data structure at a later point without requiring any changes to the user’s code. Therefore, use your existing data structure—specific operations to iterate over all your elements within your implementation, and call some provided user- function on each element during this iteration. This user-function gets the element content as a parameter and can then perform its operations on this element. The user calls just one function to trigger the iteration, and the whole iteration takes place inside your implementation. Table P-9. Patterns for organizing files in modular programs Pattern name Summary “Include Guard” on page 189 It’s easy to include a header file multiple times, but including the same header file leads to compile errors if types or certain macros are part of it, because during compilation they get redefined. Therefore, protect the content of your header files against multiple inclusion so that the developer using the header files does not have to care whether it is included multiple times. Use an interlocked #ifdef statement or a #pragma once statement to achieve this. “Software-Module Directories” on page 192 Splitting code into different files increases the number of files in your codebase. Having all files in one directory makes it difficult to keep an overview of all the files, particularly for large codebases. Therefore, put header files and implementation files that belong to a tightly coupled functionality into one directory. Name that directory after the functionality that is provided via the header files. “Global Include Directory” on page 197 To include files from other software-modules, you have to use relative paths like ../othersoftwaremodule/ file.h. You have to know the exact location of the other header file. Therefore, have one global directory in your codebase that contains all software-module APIs. Add this directory to the global include paths in your toolchain. Preface | xv
📄 Page 18
Pattern name Summary “Self-Contained Component” on page 201 From the directory structure it is not possible to see the dependencies in the code. Any software-module can simply include the header files from any other software-module, so it’s impossible to check dependencies in the code via the compiler. Therefore, identify software-modules that contain similar functionality and that should be deployed together. Put these software-modules into a common directory and have a designated subdirectory for their header files that are relevant for the caller. “API Copy” on page 207 You want to develop, version, and deploy the parts of your codebase independently from one another. However, to do that, you need clearly defined interfaces between the code parts and the ability to separate that code into different repositories. Therefore, to use the functionality of another component, copy its API. Build that other component separately and copy the build artifacts and its public header files. Put these files into a directory inside your component and configure that directory as a global include path. Table P-10. Patterns for escaping #ifdef hell Pattern name Summary “Avoid Variants” on page 220 Using different functions for each platform makes the code harder to read and write. The programmer is required to initially understand, correctly use, and test these multiple functions in order to achieve a single functionality across multiple platforms. Therefore, use standardized functions that are available on all platforms. If there are no standardized functions, consider not implementing the functionality. “Isolated Primitives” on page 224 Having code variants organized with #ifdef statements makes the code unreadable. It is very difficult to follow the program flow, because it is implemented multiple times for multiple platforms. Therefore, isolate your code variants. In your implementation file, put the code handling the variants into separate functions and call these functions from your main program logic, which then contains only platform-independent code. “Atomic Primitives” on page 227 The function that contains the variants and is called by the main program is still hard to comprehend because all the complex #ifdef code was only put into this function in order to get rid of it in the main program. Therefore, make your primitives atomic. Only handle exactly one kind of variant per function. If you handle multiple kinds of variants, for example, operating system variants and hardware variants, then have separate functions for that. “Abstraction Layer” on page 231 You want to use the functionality which handles platform variants at several places in your codebase, but you do not want to duplicate the code of that functionality. Therefore, provide an API for each functionality that requires platform-specific code. Define only platform-independent functions in the header file and put all platform-specific #ifdef code into the implementation file. The caller of your functions includes only your header file and does not have to include any platform-specific files. “Split Variant Implementations” on page 236 The platform-specific implementations still contain #ifdef statements to distinguish between code variants. That makes it difficult to see and select which part of the code should be built for which platform. Therefore, put each variant implementation into a separate implementation file and select per file what you want to compile for which platform. xvi | Preface
📄 Page 19
Conventions Used in This Book The following typographical conventions are used in this book: Italic Indicates new terms, URLs, email addresses, filenames, and file extensions. Bold Used to highlight the problem and solution for each pattern. Constant width Used for program listings, as well as within paragraphs to refer to program ele‐ ments such as variable or function names, databases, data types, environment variables, statements, and keywords. This element signifies a general note. This element indicates a warning or caution. Using Code Examples The code examples in this book show short code snippets which focus on the core idea to showcase the patterns and their application. The code snippets by themselves won’t compile, because to keep it simple several things are omitted (for example, include files). If you are interested in getting the full code which does compile, you can download it from GitHub at https://github.com/christopher-preschern/fluent-c. If you have a technical question or a problem using the code examples, please send email to bookquestions@oreilly.com. This book is here to help you get your job done. In general, if example code is offered with this book, you may use it in your programs and documentation. You do not need to contact us for permission unless you’re reproducing a significant portion of the code. For example, writing a program that uses several chunks of code from this book does not require permission. Selling or distributing examples from O’Reilly books does require permission. Answering a question by citing this book and quoting example Preface | xvii
📄 Page 20
code does not require permission. Incorporating a significant amount of example code from this book into your product’s documentation does require permission. We appreciate, but generally do not require, attribution. An attribution usually includes the title, author, publisher, and ISBN. For example: “Fluent C by Christopher Preschern (O’Reilly). Copyright 2023 Christopher Preschern, 978-1-492-09733-4.” If you feel your use of code examples falls outside fair use or the permission given above, feel free to contact us at permissions@oreilly.com. The patterns in this book all present existing code examples which apply these pat‐ terns. The following list shows the references to these code examples: • The game NetHack • OpenWrt Project • OpenSSL library • Wireshark network sniffer • Portland Pattern repository • Git version control system • Apache Portable Runtime • Apache Webserver • B&R Automation Runtime operating system (proprietary and undisclosed code of the company B&R Industrial Automation GmbH) • B&R Visual Components automation system visualization editor (proprietary and undisclosed code of the company B&R Industrial Automation GmbH) • NetDRMS data management system • MATLAB programming and numeric computing platform • GLib library • GoAccess real-time web analyzer • Cloudy physical calculation software • GNU Compiler Collection (GCC) • MySQL database system • Android ION memory manager • Windows API • Apple’s Cocoa API • VxWorks real-time operating system • sam text editor xviii | Preface
The above is a preview of the first 20 pages. Register to read the complete e-book.

💝 Support Author

0.00
Total Amount (¥)
0
Donation Count

Login to support the author

Login Now
Back to List