Professional C++ [5 ed.] 9781119695400, 9781119695509, 9781119695455, 2020950208

Improve your existing C++ competencies quickly and efficiently with this advanced volume Professional C++, 5th Edition

2,891 448 26MB

English Pages 1312 [1284] Year 2021

Report DMCA / Copyright

DOWNLOAD FILE

Polecaj historie

Professional C++ [5 ed.]
 9781119695400, 9781119695509, 9781119695455, 2020950208

Citation preview

PROFESSIONAL C++ INTRODUCTION . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xlvii

▸▸ PART I

INTRODUCTION TO PROFESSIONAL C++

CHAPTER 1

A Crash Course in C++ and the Standard Library. . . . . . . . . . . . . . . . . 3

CHAPTER 2

Working with Strings and String Views . . . . . . . . . . . . . . . . . . . . . . . . 87

CHAPTER 3

Coding with Style. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111

▸▸ PART II

PROFESSIONAL C++ SOFTWARE DESIGN

CHAPTER 4

Designing Professional C++ Programs . . . . . . . . . . . . . . . . . . . . . . . 137

CHAPTER 5

Designing with Objects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 169

CHAPTER 6

Designing for Reuse. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 187

▸▸ PART III C++ CODING THE PROFESSIONAL WAY CHAPTER 7

Memory Management . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 211

CHAPTER 8

Gaining Proficiency with Classes and Objects. . . . . . . . . . . . . . . . . . 249

CHAPTER 9

Mastering Classes and Objects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 283

CHAPTER 10 Discovering Inheritance Techniques. . . . . . . . . . . . . . . . . . . . . . . . . . 337 CHAPTER 11 Odds and Ends. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 397 CHAPTER 12 Writing Generic Code with Templates. . . . . . . . . . . . . . . . . . . . . . . . 421 CHAPTER 13 Demystifying C++ I/O . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 465 CHAPTER 14 Handling Errors. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 495 CHAPTER 15 Overloading C++ Operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 535 CHAPTER 16 Overview of the C++ Standard Library. . . . . . . . . . . . . . . . . . . . . . . 573 CHAPTER 17 Understanding Iterators and the Ranges Library. . . . . . . . . . . . . . . 603 CHAPTER 18 Standard Library Containers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 627 CHAPTER 19 Function Pointers, Function Objects, and 

Lambda Expressions. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 699 CHAPTER 20 Mastering Standard Library Algorithms. . . . . . . . . . . . . . . . . . . . . . . 725 Continues

CHAPTER 21 String Localization and Regular Expressions. . . . . . . . . . . . . . . . . . . 763 CHAPTER 22 Date and Time Utilities. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 793 CHAPTER 23 Random Number Facilities. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 809 CHAPTER 24 Additional Library Utilities. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 821

▸▸ PART IV MASTERING ADVANCED FEATURES OF C++ CHAPTER 25 Customizing and Extending the Standard Library. . . . . . . . . . . . . . . 833 CHAPTER 26 Advanced Templates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 877 CHAPTER 27 Multithreaded Programming with C++. . . . . . . . . . . . . . . . . . . . . . . 915

▸▸ PART V C++ SOFTWARE ENGINEERING CHAPTER 28 Maximizing Software Engineering Methods . . . . . . . . . . . . . . . . . . . 971 CHAPTER 29 Writing Efficient C++. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 993 CHAPTER 30 Becoming Adept at Testing. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1021 CHAPTER 31 Conquering Debugging. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1045 CHAPTER 32 Incorporating Design Techniques and Frameworks. . . . . . . . . . . . 1083 CHAPTER 33 Applying Design Patterns. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1105 CHAPTER 34 Developing Cross-Platform and Cross-Language Applications. . . . 1137

▸▸ PART VI APPENDICES APPENDIX A C++ Interviews. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1165 APPENDIX B Annotated Bibliography. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1191 APPENDIX C Standard Library Header Files. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1203 APPENDIX D Introduction to UML. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1213 INDEX . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1219

PROFESSIONAL

C++

PROFESSIONAL

C++ Fifth Edition

Marc Gregoire

Professional C++ Copyright © 2021 by John Wiley & Sons, Inc., Indianapolis, Indiana Published simultaneously in Canada and the United Kingdom ISBN: 978-1-119-69540-0 ISBN: 978-1-119-69550-9 (ebk) ISBN: 978-1-119-69545-5 (ebk) Manufactured in the United States of America No part of this publication may be reproduced, stored in a retrieval system or transmitted in any form or by any means, electronic, mechanical, photocopying, recording, scanning or otherwise, except as permitted under Sections 107 or 108 of the 1976 United States Copyright Act, without either the prior written permission of the Publisher, or authorization through payment of the appropriate per-copy fee to the Copyright Clearance Center, 222 Rosewood Drive, Danvers, MA 01923, (978) 750-8400, fax (978) 646-8600. Requests to the Publisher for permission should be addressed to the Permissions Department, John Wiley & Sons, Inc., 111 River Street, Hoboken, NJ 07030, (201) 748-6011, fax (201) 748-6008, or online at www.wiley.com/go/permissions. Limit of Liability/Disclaimer of Warranty: The publisher and the author make no representations or warranties with respect to the accuracy or completeness of the contents of this work and specifically disclaim all warranties, including without limitation warranties of fitness for a particular purpose. No warranty may be created or extended by sales or promotional materials. The advice and strategies contained herein may not be suitable for every situation. This work is sold with the understanding that the publisher is not engaged in rendering legal, accounting, or other professional services. If professional assistance is required, the services of a competent professional person should be sought. Neither the publisher nor the author shall be liable for damages arising herefrom. The fact that an organization or Web site is referred to in this work as a citation and/or a potential source of further information does not mean that the author or the publisher endorses the information the organization or Web site may provide or recommendations it may make. Further, readers should be aware that Internet Web sites listed in this work may have changed or disappeared between when this work was written and when it is read. For general information on our other products and services please contact our Customer Care Department within the United States at (877) 762-2974, outside the United States at (317) 572-3993 or fax (317) 572-4002. Wiley publishes in a variety of print and electronic formats and by print-on-demand. Some material included with standard print versions of this book may not be included in e-books or in print-on-demand. If this book refers to media such as a CD or DVD that is not included in the version you purchased, you may download this material at booksupport.wiley.com. For more information about Wiley products, visit www.wiley.com. Library of Congress Control Number: 2020950208 Trademarks: Wiley, the Wiley logo, Wrox, the Wrox logo, Programmer to Programmer, and related trade dress are trademarks or registered trademarks of John Wiley & Sons, Inc. and/or its affiliates, in the United States and other countries, and may not be used without written permission. All other trademarks are the property of their respective owners. John Wiley & Sons, Inc., is not associated with any product or vendor mentioned in this book.

Dedicated to my wonderful parents and my brother, who are always there for me. Their support and patience helped me in finishing this book.

ABOUT THE AUTHOR

MARC GREGOIRE  is a software architect from Belgium. He graduated from the University of Leuven, Belgium, with a degree in “Burgerlijk ingenieur in de computer wetenschappen” (equivalent to a master of science in engineering in computer science). The year after, he received an advanced master’s degree in artificial intelligence, cum laude, at the same university. After his studies, Marc started working for a software consultancy company called Ordina Belgium. As a consultant, he worked for Siemens and Nokia Siemens Networks on critical 2G and 3G software running on Solaris for telecom operators. This required working in international teams stretching from South America and the United States to Europe, the Middle East, Africa, and Asia. Now, Marc is a software architect at Nikon Metrology (nikonmetrology.com), a division of Nikon and a leading provider of precision optical instruments, X-ray machines, and metrology solutions for X-ray, CT, and 3-D geometric inspection.

His main expertise is C/C++, specifically Microsoft VC++ and the MFC framework. He has experience in developing C++ programs running 24/7 on Windows and Linux platforms: for example, KNX/EIB home automation software. In addition to C/C++, Marc also likes C#. Since April 2007, he has received the annual Microsoft MVP (Most Valuable Professional) award for his Visual C++ expertise. Marc is the founder of the Belgian C++ Users Group (becpp.org), co-author of C++ Standard Library Quick Reference 1st and 2nd editions (Apress), a technical editor for numerous books for several publishers, and a regular speaker at the CppCon C++ conference. He maintains a blog at www.nuonsoft.com/blog/ and is passionate about traveling and gastronomic restaurants.

ABOUT THE TECHNICAL EDITORS

PETER VAN WEERT  is a Belgian software engineer whose main interests and expertise are application

software development, programming languages, algorithms, and data structures. He received his master of science degree in computer science summa cum laude with congratulations from the Board of Examiners from the University of Leuven. In 2010, he completed his PhD thesis on the design and efficient compilation of rule-based programming languages at the research group for declarative programming languages and artificial intelligence. During his doctoral studies he was a teaching assistant for object-oriented programming (Java), software analysis and design, and declarative programming. Peter then joined Nikon Metrology, where he worked on large-scale, industrial application software in the area of 3-D laser scanning and point cloud inspection for over six years. Today, Peter is senior C++ engineer and Scrum team leader at Medicim, the R&D unit for digital dentistry software of Envista Holdings. At Medicim, he codevelops a suite of applications for dental professionals, capable of capturing patient data from a wide range of hardware, with advanced diagnostic functionality and support for implant planning and prosthetic design. Common themes in his professional career include advanced desktop application development, mastering and refactoring of code bases of millions of lines of C++ code, high-performant, real-time processing of 3-D data, concurrency, algorithms and data structures, interfacing with cutting-edge hardware, and leading agile development teams. Peter is a regular speaker at, and board member of, the Belgian C++ Users Group. He also co-authored two books: C++ Standard Library Quick Reference and Beginning C++ (5th edition), both published by Apress. OCKERT J. DU PREEZ is a self-taught developer who started learning programming in the days of QBasic. He has written hundreds of developer articles over the years detailing his programming quests and adventures. His articles can be found on CodeGuru (codeguru.com), Developer.com (developer.com), DevX (devx.com), and Database Journal (databasejournal.com). Software development is his second love, just after his wife and child.

He knows a broad spectrum of development languages including C++, C#, VB.NET, JavaScript, and HTML. He has written the books Visual Studio 2019 In-Depth (BpB Publications) and JavaScript for Gurus (BpB Publications). He was a Microsoft Most Valuable Professional for .NET (2008–2017).

ACKNOWLEDGMENTS

I THANK THE JOHN WILEY & SONS AND WROX PRESS  editorial and production teams for their support. Especially, thank you to Jim Minatel, executive editor at Wiley, for giving me a chance to write this fifth edition; Kelly Talbot, project editor, for managing this project; and Kim Wimpsett, copy editor, for improving readability and consistency and making sure the text is grammatically correct.

Thanks to technical editor Hannes Du Preez for checking the technical accuracy of the book. His contributions in strengthening this book are greatly appreciated. A very special thank you to technical editor Peter Van Weert for his outstanding contributions. His considerable advice and insights have truly elevated this book to a higher level. Of course, the support and patience of my parents and my brother were very important in finishing this book. I would also like to express my sincere gratitude to my employer, Nikon Metrology, for supporting me during this project. Finally, I thank you, the reader, for trying this approach to professional C++ software development.

—Marc Gregoire

CONTENTS

INTRODUCTION

xlvii

PART I: INTRODUCTION TO PROFESSIONAL C++ CHAPTER 1: A CRASH COURSE IN C++ AND THE STANDARD LIBRARY 3

C++ Crash Course

4

The Obligatory “Hello, World” Program 4 Comments 5 Importing Modules 5 Preprocessor Directives 5 The main() Function 8 I/O Streams 8 Namespaces 9 Nested Namespace 11 Namespace Alias 11 Literals 11 Variables 12 Numerical Limits 14 Zero Initialization 15 Casting 15 Floating-Point Numbers 16 Operators 16 Enumerated Types 19 Old-Style Enumerated Types 21 Structs 22 Conditional Statements 23 if/else Statements 23 switch Statements 24 The Conditional Operator 25 Logical Evaluation Operators 26 Three-Way Comparisons 27 Functions 28 Function Return Type Deduction 30 Current Function’s Name 30 Function Overloading 30

Contents

Attributes 30 [[nodiscard]] 31 [[maybe_unused]] 31 [[noreturn]] 32 [[deprecated]] 32 [[likely]] and [[unlikely]] 33 C-Style Arrays 33 std::array 35 std::vector 36 std::pair 36 std::optional 37 Structured Bindings 38 Loops 38 The while Loop 38 The do/while Loop 39 The for Loop 39 The Range-Based for Loop 39 Initializer Lists 40 Strings in C++ 40 C++ as an Object-Oriented Language 41 Defining Classes 41 Using Classes 44 Scope Resolution 44 Uniform Initialization 45 Designated Initializers 48 Pointers and Dynamic Memory 49 The Stack and the Free Store 49 Working with Pointers 50 Dynamically Allocated Arrays 51 Null Pointer Constant 52 The Use of const 53 const as a Qualifier for a Type 53 const Methods 55 The constexpr Keyword 56 The consteval Keyword 57 References 58 Reference Variables 58 Reference Data Members 61 Reference Parameters 61 Reference Return Values 64 Deciding Between References and Pointers 64

xvi

Contents

const_cast() 68 Exceptions 69 Type Aliases 70 typedefs 71 Type Inference 72 The auto Keyword 72 The decltype Keyword 75 The Standard Library 75

Your First Bigger C++ Program

75

An Employee Records System 76 The Employee Class 76 Employee.cppm 76 Employee.cpp 78 EmployeeTest.cpp 79 The Database Class 80 Database.cppm 80 Database.cpp 81 DatabaseTest.cpp 82 The User Interface 82 Evaluating the Program 85

Summary 85 Exercises 85 CHAPTER 2: WORKING WITH STRINGS AND STRING VIEWS

Dynamic Strings C-Style Strings String Literals Raw String Literals The C++ std::string Class What Is Wrong with C-Style Strings? Using the string Class std::string Literals CTAD with std::vector and Strings Numeric Conversions High-Level Numeric Conversions Low-Level Numeric Conversions The std::string_view Class std::string_view and Temporary Strings std::string_view Literals Nonstandard Strings

87

88 88 90 90 92 92 92 95 96 96 96 97 100 102 102 102

String Formatting

103

Format Specifiers

104 xvii

Contents

width 104 [fill]align 105 sign 105 # 105 type 106 precision 107 0 107 Format Specifier Errors 107 Support for Custom Types 107

Summary 110 Exercises 110 CHAPTER 3: CODING WITH STYLE

111

The Importance of Looking Good

111

Thinking Ahead Elements of Good Style

Documenting Your Code Reasons to Write Comments Commenting to Explain Usage Commenting to Explain Complicated Code Commenting to Convey Meta-information Commenting Styles Commenting Every Line Prefix Comments Fixed-Format Comments Ad Hoc Comments Self-Documenting Code

112 112

112 112 112 115 116 117 117 118 119 120 122

Decomposition 122 Decomposition Through Refactoring Decomposition by Design Decomposition in This Book

123 124 124

Naming 124 Choosing a Good Name 124 Naming Conventions 125 Counters 125 Prefixes 126 Hungarian Notation 126 Getters and Setters 127 Capitalization 127 Namespaced Constants 127

Using Language Features with Style Use Constants xviii

127 128

Contents

Use References Instead of Pointers Use Custom Exceptions

128 129

Formatting 129 The Curly Brace Alignment Debate Coming to Blows over Spaces and Parentheses Spaces, Tabs, and Line Breaks

130 131 131

Stylistic Challenges 132 Summary 132 Exercises 133 PART II: PROFESSIONAL C++ SOFTWARE DESIGN CHAPTER 4: DESIGNING PROFESSIONAL C++ PROGRAMS

What Is Programming Design? The Importance of Programming Design Designing for C++ Two Rules for Your Own C++ Designs

137

138 139 141 142

Abstraction 142 Benefiting from Abstraction 142 Incorporating Abstraction in Your Design 143 Reuse 144 Writing Reusable Code 144 Reusing Designs 145

Reusing Existing Code

146

A Note on Terminology 146 Deciding Whether to Reuse Code or Write it Yourself 147 Advantages to Reusing Code 147 Disadvantages to Reusing Code 148 Putting It Together to Make a Decision 149 Guidelines for Choosing a Library to Reuse 149 Understand the Capabilities and Limitations 149 Understand the Learning Cost 150 Understand the Performance 150 Understand Platform Limitations 153 Understand Licensing 153 Understand Support and Know Where to Find Help 154 Prototype 154 Open-Source Libraries 155 The C++ Standard Library 157

xix

Contents

Designing a Chess Program

157

Requirements 158 Design Steps 158 Divide the Program into Subsystems 158 Choose Threading Models 160 Specify Class Hierarchies for Each Subsystem 161 Specify Classes, Data Structures, Algorithms, and Patterns for  Each Subsystem 162 Specify Error Handling for Each Subsystem 165

Summary 166 Exercises 166 CHAPTER 5: DESIGNING WITH OBJECTS

Am I Thinking Procedurally? The Object-Oriented Philosophy

169

170 170

Classes 170 Components 171 Properties 171 Behaviors 172 Bringing It All Together 172

Living in a World of Classes

173

Over-Classification 173 Overly General Classes 174

Class Relationships

175

The Has-a Relationship 175 The Is-a Relationship (Inheritance) 176 Inheritance Techniques 177 Polymorphism 178 The Fine Line Between Has-a and Is-a 178 The Not-a Relationship 181 Hierarchies 182 Multiple Inheritance 183 Mixin Classes 184

Summary 185 Exercises 185 CHAPTER 6: DESIGNING FOR REUSE

The Reuse Philosophy How to Design Reusable Code Use Abstraction Structure Your Code for Optimal Reuse Avoid Combining Unrelated or Logically Separate Concepts xx

187

188 189 189 191 191

Contents

Use Templates for Generic Data Structures and Algorithms Provide Appropriate Checks and Safeguards Design for Extensibility Design Usable Interfaces Consider the Audience Consider the Purpose Design Interfaces That Are Easy to Use Design General-Purpose Interfaces Reconciling Generality and Ease of Use Designing a Successful Abstraction The SOLID Principles

193 195 196 198 198 199 200 204 205 205 206

Summary 207 Exercises 207 PART III: C++ CODING THE PROFESSIONAL WAY CHAPTER 7: MEMORY MANAGEMENT

Working with Dynamic Memory

211

212

How to Picture Memory 212 Allocation and Deallocation 213 Using new and delete 213 What About My Good Friend malloc? 214 When Memory Allocation Fails 215 Arrays 215 Arrays of Primitive Types 215 Arrays of Objects 218 Deleting Arrays 218 Multidimensional Arrays 219 Working with Pointers 223 A Mental Model for Pointers 223 Casting with Pointers 224

Array-Pointer Duality Arrays Are Pointers! Not All Pointers Are Arrays!

Low-Level Memory Operations Pointer Arithmetic Custom Memory Management Garbage Collection Object Pools

Common Memory Pitfalls Underallocating Data Buffers and Out-of-Bounds Memory Access

224 224 226

227 227 228 228 229

229 229 xxi

Contents

Memory Leaks Finding and Fixing Memory Leaks in Windows with Visual C++ Finding and Fixing Memory Leaks in Linux with Valgrind Double-Deletion and Invalid Pointers

Smart Pointers

231 232 233 234

234

unique_ptr 235 Creating unique_ptrs 236 Using unique_ptrs 237 unique_ptr and C-Style Arrays 238 Custom Deleters 239 shared_ptr 239 Creating and Using shared_ptrs 239 The Need for Reference Counting 241 Casting a shared_ptr 242 Aliasing 242 weak_ptr 243 Passing to Functions 244 Returning from Functions 244 enable_shared_from_this 244 The Old and Removed auto_ptr 245

Summary 246 Exercises 246 CHAPTER 8: GAINING PROFICIENCY WITH CLASSES  AND OBJECTS

Introducing the Spreadsheet Example Writing Classes Class Definitions Class Members Access Control Order of Declarations In-Class Member Initializers Defining Methods Accessing Data Members Calling Other Methods The this Pointer Using Objects Objects on the Stack Objects on the Free Store

Understanding Object Life Cycles Object Creation Writing Constructors Using Constructors xxii

249

250 250 250 251 251 252 253 253 254 254 255 257 257 257

258 258 259 260

Contents

Providing Multiple Constructors Default Constructors Constructor Initializers Copy Constructors Initializer-List Constructors Delegating Constructors Converting Constructors and Explicit Constructors Summary of Compiler-Generated Constructors Object Destruction Assigning to Objects Declaring an Assignment Operator Defining an Assignment Operator Explicitly Defaulted and Deleted Assignment Operator Compiler-Generated Copy Constructor and Copy Assignment Operator Distinguishing Copying from Assignment Objects as Return Values Copy Constructors and Object Members

260 261 265 269 271 273 273 275 276 277 278 278 280 280 280 280 281

Summary 282 Exercises 282 CHAPTER 9: MASTERING CLASSES AND OBJECTS

283

Friends 284 Dynamic Memory Allocation in Objects 285 The Spreadsheet Class Freeing Memory with Destructors Handling Copying and Assignment The Spreadsheet Copy Constructor The Spreadsheet Assignment Operator Disallowing Assignment and Pass-by-Value Handling Moving with Move Semantics Rvalue References Implementing Move Semantics Testing the Spreadsheet Move Operations Implementing a Swap Function with Move Semantics Using std::move() in Return Statements Optimal Way to Pass Arguments to Functions Rule of Zero

More About Methods static Methods const Methods mutable Data Members

285 288 289 291 291 294 295 295 297 301 303 303 304 305

306 306 307 308 xxiii

Contents

Method Overloading Overloading Based on const Explicitly Deleting Overloads Ref-Qualified Methods Inline Methods Default Arguments

308 309 310 310 311 313

Different Kinds of Data Members

314

static Data Members Inline Variables Accessing static Data Members within Class Methods Accessing static Data Members Outside Methods const static Data Members Reference Data Members

Nested Classes Enumerated Types Inside Classes Operator Overloading Example: Implementing Addition for SpreadsheetCells First Attempt: The add Method Second Attempt: Overloaded operator+ as a Method Third Attempt: Global operator+ Overloading Arithmetic Operators Overloading the Arithmetic Shorthand Operators Overloading Comparison Operators Compiler-Generated Comparison Operators Building Types with Operator Overloading

Building Stable Interfaces Using Interface and Implementation Classes

314 314 315 316 316 317

318 319 320 320 320 321 322 324 324 325 328 330

330 330

Summary 334 Exercises 335 CHAPTER 10: DISCOVERING INHERITANCE TECHNIQUES

Building Classes with Inheritance Extending Classes A Client’s View of Inheritance A Derived Class’s View of Inheritance Preventing Inheritance Overriding Methods The virtual Keyword Syntax for Overriding a Method A Client’s View of Overridden Methods The override Keyword xxiv

337

338 338 339 340 341 342 342 342 343 344

Contents

The Truth About virtual Preventing Overriding

Inheritance for Reuse The WeatherPrediction Class Adding Functionality in a Derived Class Replacing Functionality in a Derived Class

Respect Your Parents

346 350

350 350 351 352

353

Parent Constructors Parent Destructors Referring to Parent Names Casting Up and Down

353 355 356 358

Inheritance for Polymorphism

360

Return of the Spreadsheet Designing the Polymorphic Spreadsheet Cell The SpreadsheetCell Base Class A First Attempt Pure Virtual Methods and Abstract Base Classes The Individual Derived Classes StringSpreadsheetCell Class Definition StringSpreadsheetCell Implementation DoubleSpreadsheetCell Class Definition and Implementation Leveraging Polymorphism Future Considerations

Multiple Inheritance Inheriting from Multiple Classes Naming Collisions and Ambiguous Base Classes Name Ambiguity Ambiguous Base Classes Uses for Multiple Inheritance

Interesting and Obscure Inheritance Issues Changing the Overridden Method’s Return Type Adding Overloads of Virtual Base Class Methods to Derived Classes Inherited Constructors Hiding of Inherited Constructors Inherited Constructors and Multiple Inheritance Initialization of Data Members Special Cases in Overriding Methods The Base Class Method Is static The Base Class Method Is Overloaded The Base Class Method Is private The Base Class Method Has Default Arguments

360 360 361 361 362 363 363 363 364 364 365

367 367 368 368 369 371

371 371 373 374 375 376 377 378 378 379 380 382

xxv

Contents

The Base Class Method Has a Different Access Specification Copy Constructors and Assignment Operators in Derived Classes Run-Time Type Facilities Non-public Inheritance Virtual Base Classes

383 385 386 388 389

Casts 390 static_cast() 390 reinterpret_cast() 391 std::bit_cast() 392 dynamic_cast() 393 Summary of Casts 394

Summary 394 Exercises 395 CHAPTER 11: ODDS AND ENDS

397

Modules 397 Module Interface Files 399 Module Implementation Files 401 Splitting Interface from Implementation 402 Visibility vs. Reachability 403 Submodules 404 Module Partitions 405 Implementation Partitions 407 Header Units 408

Header Files Duplicate Definitions Circular Dependencies Querying Existence of Headers

Feature Test Macros for Core Language Features The static Keyword static Data Members and Methods static Linkage The extern Keyword static Variables in Functions Order of Initialization of Nonlocal Variables Order of Destruction of Nonlocal Variables

xxvi

408 409 409 410

410 411 411 411 413 414 415 415

Contents

C Utilities Variable-Length Argument Lists Accessing the Arguments Why You Shouldn’t Use C-Style Variable-Length Argument Lists Preprocessor Macros

415 415 416 417 417

Summary 419 Exercises 419 CHAPTER 12: WRITING GENERIC CODE WITH TEMPLATES

Overview of Templates Class Templates Writing a Class Template Coding Without Templates A Template Grid Class Using the Grid Template How the Compiler Processes Templates Selective Instantiation Template Requirements on Types Distributing Template Code Between Files Method Definitions in Same File as Class Template Definition Method Definitions in Separate File Template Parameters Non-type Template Parameters Default Values for Type Parameters Class Template Argument Deduction Method Templates Method Templates with Non-type Parameters Class Template Specialization Deriving from Class Templates Inheritance vs. Specialization Alias Templates

Function Templates Function Template Overloading Friend Function Templates of Class Templates More on Template Parameter Deduction Return Type of Function Templates Abbreviated Function Template Syntax

421

422 422 423 423 426 430 431 431 432 432 433 433 433 434 436 436 438 440 442 445 446 447

447 449 449 451 451 453

xxvii

Contents

Variable Templates 454 Concepts 454 Syntax 455 Constraints Expression 455 Requires Expressions 455 Combining Concept Expressions 457 Predefined Standard Concepts 457 Type-Constrained auto 458 Type Constraints and Function Templates 458 Constraint Subsumption 460 Type Constraints and Class Templates 461 Type Constraints and Class Methods 461 Type Constraints and Template Specialization 462

Summary 463 Exercises 463 CHAPTER 13: DEMYSTIFYING C++ I/O

Using Streams What Is a Stream, Anyway? Stream Sources and Destinations Output with Streams Output Basics Methods of Output Streams Handling Output Errors Output Manipulators Input with Streams Input Basics Handling Input Errors Input Methods Input Manipulators Input and Output with Objects Custom Manipulators

String Streams File Streams Text Mode vs. Binary Mode Jumping Around with seek() and tell() Linking Streams Together

Bidirectional I/O Filesystem Support Library

465

466 466 467 468 468 469 470 471 473 473 475 476 480 481 482

482 484 485 485 487

488 490

Path 490 Directory Entry 491 xxviii

Contents

Helper Functions Directory Iteration

492 492

Summary 493 Exercises 493 CHAPTER 14: HANDLING ERRORS

Errors and Exceptions

495

496

What Are Exceptions, Anyway? 496 Why Exceptions in C++ Are a Good Thing 496 Recommendation 498

Exception Mechanics Throwing and Catching Exceptions Exception Types Catching Exception Objects as Reference-to-const Throwing and Catching Multiple Exceptions Matching and const Matching Any Exception Uncaught Exceptions noexcept Specifier noexcept(expression) Specifier noexcept(expression) Operator Throw Lists

Exceptions and Polymorphism The Standard Exception Hierarchy Catching Exceptions in a Class Hierarchy Writing Your Own Exception Classes Source Location Nested Exceptions

Rethrowing Exceptions Stack Unwinding and Cleanup

498 499 501 502 503 505 505 505 507 508 508 508

509 509 510 512 514 517

519 520

Use Smart Pointers Catch, Cleanup, and Rethrow

521 522

Common Error-Handling Issues

523

Memory Allocation Errors Non-throwing new Customizing Memory Allocation Failure Behavior Errors in Constructors Function-Try-Blocks for Constructors Errors in Destructors

523 524 524 526 528 531

Summary 531 Exercises 532 xxix

Contents

CHAPTER 15: OVERLOADING C++ OPERATORS

Overview of Operator Overloading

536

Why Overload Operators? Limitations to Operator Overloading Choices in Operator Overloading Method or Global Function Choosing Argument Types Choosing Return Types Choosing Behavior Operators You Shouldn’t Overload Summary of Overloadable Operators Rvalue References Precedence and Associativity Relational Operators

536 536 537 537 538 538 539 539 540 544 545 546

Overloading the Arithmetic Operators

547

Overloading Unary Minus and Unary Plus Overloading Increment and Decrement

Overloading the Bitwise and Binary Logical Operators Overloading the Insertion and Extraction Operators Overloading the Subscripting Operator

547 547

548 549 550

Providing Read-Only Access with operator[] Non-integral Array Indices

553 555

Overloading the Function Call Operator Overloading the Dereferencing Operators

555 557

Implementing operator* Implementing operator–> What in the World Are operator.* and operator–>*?

Writing Conversion Operators Operator auto Solving Ambiguity Problems with Explicit Conversion Operators Conversions for Boolean Expressions

Overloading the Memory Allocation and Deallocation Operators How new and delete Really Work The New-Expression and operator new The Delete-Expression and operator delete Overloading operator new and operator delete Explicitly Deleting/Defaulting operator new and operator delete Overloading operator new and operator delete with Extra Parameters Overloading operator delete with Size of Memory as Parameter

Overloading User-Defined Literal Operators

xxx

535

558 558 559

559 560 561 561

563 564 564 565 565 568 568 569

570

Contents

Cooked-Mode Literal Operator Raw-Mode Literal Operator Standard User-Defined Literals

570 571 571

Summary 572 Exercises 572 CHAPTER 16: OVERVIEW OF THE C++ STANDARD LIBRARY

Coding Principles Use of Templates Use of Operator Overloading

Overview of the C++ Standard Library

573

574 574 575

575

Strings 575 Regular Expressions 576 I/O Streams 576 Smart Pointers 576 Exceptions 576 Numerics Library 577 Time and Date Utilities 579 Random Numbers 579 Initializer Lists 579 Pair and Tuple 579 Vocabulary Types 580 Function Objects 580 Filesystem 580 Multithreading 580 Type Traits 581 Standard Integer Types 581 Standard Library Feature Test Macros 581 582 Source Location 582 Containers 582 vector 583 list 584 forward_list 584 deque 584 array 584 span 585 queue 585 priority_queue 585 stack 586

xxxi

Contents

set and multiset 586 map and multimap 587 Unordered Associative Containers/Hash Tables 587 bitset 588 Summary of Standard Library Containers 588 Algorithms 591 Nonmodifying Sequence Algorithms 591 Modifying Sequence Algorithms 593 Operational Algorithms 595 Swap Algorithms 595 Partition Algorithms 595 Sorting Algorithms 596 Binary Search Algorithms 597 Set Algorithms 597 Heap Algorithms 598 Minimum/Maximum Algorithms 598 Numerical Processing Algorithms 599 Permutation Algorithms 600 Choosing an Algorithm 600 Ranges Library 601 What’s Missing from the Standard Library 601

Summary 601 Exercises 601 CHAPTER 17: UNDERSTANDING ITERATORS AND  THE RANGES LIBRARY

603

Iterators 604 Getting Iterators for Containers 606 Iterator Traits 608 Examples 609

Stream Iterators Output Stream Iterator Input Stream Iterator

610 610 611

Iterator Adapters

612

Insert Iterators Reverse Iterators Move Iterators

612 614 615

Ranges 616 Range-Based Algorithms 617 Projection 618 Views 619

xxxii

Contents

Modifying Elements Through a View Mapping Elements Range Factories Input Streams as Views

622 623 623 625

Summary 625 Exercises 626 CHAPTER 18: STANDARD LIBRARY CONTAINERS

Containers Overview Requirements on Elements Exceptions and Error Checking

Sequential Containers

627

628 628 630

631

vector 631 vector Overview 631 vector Details 633 Move Semantics 646 vector Example: A Round-Robin Class 647 The vector Specialization 652 deque 653 list 653 Accessing Elements 653 Iterators 654 Adding and Removing Elements 654 list Size 654 Special list Operations 654 list Example: Determining Enrollment 656 forward_list 657 array 660 span 661

Container Adapters

663

queue 663 queue Operations 663 queue Example: A Network Packet Buffer 664 priority_queue 666 priority_queue Operations 666 priority_queue Example: An Error Correlator 667 stack 668 stack Operations 668 stack Example: Revised Error Correlator 669

Ordered Associative Containers

669

The pair Utility Class 669 map 670 xxxiii

Contents

Constructing maps 670 Inserting Elements 671 map Iterators 674 Looking Up Elements 675 Removing Elements 675 Nodes 676 map Example: Bank Account 676 multimap 679 multimap Example: Buddy Lists 680 set 682 set Example: Access Control List 682 multiset 684

Unordered Associative Containers or Hash Tables

684

Hash Functions 684 unordered_map 686 unordered_map Example: Phone Book 689 unordered_multimap 690 unordered_set/unordered_multiset 691

Other Containers

691

Standard C-Style Arrays 691 Strings 692 Streams 693 bitset 693 bitset Basics 693 Bitwise Operators 694 bitset Example: Representing Cable Channels 694

Summary 697 Exercises 698 CHAPTER 19: FUNCTION POINTERS, FUNCTION OBJECTS, AND LAMBDA EXPRESSIONS

699

Function Pointers 700 Pointers to Methods (and Data Members) 702 std::function 703 Function Objects 705 Writing Your First Function Object Function Objects in the Standard Library Arithmetic Function Objects Comparison Function Objects Logical Function Objects Bitwise Function Objects Adapter Function Objects xxxiv

705 706 706 707 709 709 709

Contents

Lambda Expressions

713

Syntax 713 Lambda Expressions as Parameters 718 Generic Lambda Expressions 719 Lambda Capture Expressions 719 Templated Lambda Expressions 720 Lambda Expressions as Return Type 721 Lambda Expressions in Unevaluated Contexts 722 Default Construction, Copying, and Assigning 722

Invokers 722 Summary 723 Exercises 723 CHAPTER 20: MASTERING STANDARD LIBRARY ALGORITHMS

Overview of Algorithms The find and find_if Algorithms The accumulate Algorithm Move Semantics with Algorithms Algorithm Callbacks

Algorithm Details

725

726 726 729 730 730

731

Non-modifying Sequence Algorithms 731 Search Algorithms 731 Specialized Searchers 733 Comparison Algorithms 733 Counting Algorithms 736 Modifying Sequence Algorithms 737 generate 737 transform 738 copy 739 move 740 replace 742 erase 742 remove 743 unique 744 shuffle 745 sample 745 reverse 746 Shifting Elements 746 Operational Algorithms 747 for_each 747 for_each_n 749 xxxv

Contents

Partition Algorithms 749 Sorting Algorithms 750 Binary Search Algorithms 751 Set Algorithms 752 Minimum/Maximum Algorithms 755 Parallel Algorithms 756 Constrained Algorithms 758 Numerical Processing Algorithms 758 iota 759 Reduce Algorithms 759 Scan Algorithms 760

Summary 761 Exercises 761 CHAPTER 21: STRING LOCALIZATION AND REGULAR EXPRESSIONS 763

Localization 763 Wide Characters 764 Localizing String Literals 764 Non-Western Character Sets 765 Locales and Facets 767 Using Locales 767 Global Locale 769 Character Classification 769 Character Conversion 769 Using Facets 770 Conversions 771

Regular Expressions

772

ECMAScript Syntax 773 Anchors 773 Wildcards 773 Alternation 773 Grouping 774 Repetition 774 Precedence 775 Character Set Matches 775 Word Boundaries 777 Back References 778 Lookahead 778 Regular Expressions and Raw String Literals 778 Common Regular Expressions 779 The regex Library 779 xxxvi

Contents

regex_match() 781 regex_match() Example 781 regex_search() 783 regex_search() Example 784 regex_iterator 784 regex_iterator Example 785 regex_token_iterator 785 regex_token_iterator Examples 786 regex_replace() 788 regex_replace() Examples 789

Summary 790 Exercises 791 CHAPTER 22: DATE AND TIME UTILITIES

793

Compile-Time Rational Numbers 794 Duration 796 Clock 801 Time Point 802 Date 804 Time Zone 807 Summary 808 Exercises 808 CHAPTER 23: RANDOM NUMBER FACILITIES

809

C-Style Random Number Generation 810 Random Number Engines 811 Random Number Engine Adapters 813 Predefined Engines and Engine Adapters 813 Generating Random Numbers 814 Random Number Distributions 816 Summary 819 Exercises 819 CHAPTER 24: ADDITIONAL LIBRARY UTILITIES

Vocabulary Types

821

821

variant 821 any 823

Tuples 824 Decompose Tuples 826 Structured Bindings 827 tie 827 xxxvii

Contents

Concatenation 828 Comparisons 828 make_from_tuple 829 apply 829

Summary 829 Exercises 830 PART IV: MASTERING ADVANCED FEATURES OF C++ CHAPTER 25: CUSTOMIZING AND EXTENDING THE STANDARD LIBRARY

833

Allocators 834 Extending the Standard Library 835 Why Extend the Standard Library? 835 Writing a Standard Library Algorithm 836 find_all() 836 Writing a Standard Library Container 837 A Basic Directed Graph 837 Making directed_graph a Standard Library Container 848 Adding Support for Allocators 866 Improving graph_node 871 Additional Standard Library-Like Functionality 872 Further Improvements 874 Other Container Types 874

Summary 875 Exercises 875 CHAPTER 26: ADVANCED TEMPLATES

877

More About Template Parameters

878

More About Template Type Parameters Introducing Template Template Parameters More About Non-type Template Parameters

Class Template Partial Specialization Emulating Function Partial Specialization with Overloading Template Recursion An N-Dimensional Grid: First Attempt A Real N-Dimensional Grid

Variadic Templates Type-Safe Variable-Length Argument Lists

xxxviii

878 880 882

884 888 889 889 890

892 893

Contents

Variable Number of Mixin Classes Fold Expressions

895 896

Metaprogramming 898 Factorial at Compile Time Loop Unrolling Printing Tuples constexpr if Using a Compile-Time Integer Sequence with Folding Type Traits Using Type Categories Using Type Relationships Using the conditional Type Trait Using enable_if Using constexpr if to Simplify enable_if Constructs Logical Operator Traits Static Assertions Metaprogramming Conclusion

898 899 900 902 903 903 905 907 907 909 910 912 912 913

Summary 913 Exercises 913 CHAPTER 27: MULTITHREADED PROGRAMMING WITH C++

915

Introduction 916 Race Conditions 918 Tearing 919 Deadlocks 919 False-Sharing 920

Threads 921 Thread with Function Pointer Thread with Function Object Thread with Lambda Thread with Member Function Thread Local Storage Canceling Threads Automatically Joining Threads Retrieving Results from Threads Copying and Rethrowing Exceptions

Atomic Operations Library Atomic Operations Atomic Smart Pointers Atomic References Using Atomic Types Waiting on Atomic Variables

921 922 924 924 924 925 925 926 926

929 931 932 932 933 935 xxxix

Contents

Mutual Exclusion

936

Mutex Classes 936 Spinlock 936 Non-timed Mutex Classes 937 Timed Mutex Classes 939 Locks 939 lock_guard 939 unique_lock 940 shared_lock 941 Acquiring Multiple Locks at Once 941 scoped_lock 942 std::call_once 942 Examples Using Mutual Exclusion Objects 943 Thread-Safe Writing to Streams 943 Using Timed Locks 945 Double-Checked Locking 946

Condition Variables Spurious Wake-Ups Using Condition Variables

947 948 949

Latches 950 Barriers 951 Semaphores 951 Futures 952 std::promise and std::future 953 std::packaged_task 954 std::async 955 Exception Handling 956 std::shared_future 956

Example: Multithreaded Logger Class 958 Thread Pools 962 Coroutines 963 Threading Design and Best Practices 965 Summary 966 Exercises 966 PART V: C++ SOFTWARE ENGINEERING CHAPTER 28: MAXIMIZING SOFTWARE ENGINEERING METHODS

The Need for Process Software Life Cycle Models The Waterfall Model xl

971

972 973 973

Contents

Benefits of the Waterfall Model 974 Drawbacks of the Waterfall Model 974 Sashimi Model 975 Spiral-like Models 975 Benefits of a Spiral-like Model 976 Drawbacks of a Spiral-like Model 977 Agile 978

Software Engineering Methodologies

978

The Unified Process 979 The Rational Unified Process 980 RUP as a Product 980 RUP as a Process 980 RUP in Practice 981 Scrum 981 Roles 981 The Process 982 Benefits of Scrum 983 Drawbacks of Scrum 983 eXtreme Programming 984 XP in Theory 984 XP in Practice 988 Software Triage 988

Building Your Own Process and Methodology

989

Be Open to New Ideas Bring New Ideas to the Table Recognize What Works and What Doesn’t Work Don’t Be a Renegade

989 989 989 989

Source Code Control 990 Summary 992 Exercises 992 CHAPTER 29: WRITING EFFICIENT C++

Overview of Performance and Efficiency Two Approaches to Efficiency Two Kinds of Programs Is C++ an Inefficient Language?

Language-Level Efficiency Handle Objects Efficiently Pass-by-Value or Pass-by-Reference Return-by-Value or Return-by-Reference Catch Exceptions by Reference

993

994 994 994 994

995 996 996 998 998 xli

Contents

Use Move Semantics Avoid Creating Temporary Objects Return-Value Optimization Pre-allocate Memory Use Inline Methods and Functions

Design-Level Efficiency Cache Where Necessary Use Object Pools An Object Pool Implementation Using the Object Pool

998 998 999 1000 1001

1001 1002 1003 1003 1006

Profiling 1008 Profiling Example with gprof First Design Attempt Profiling the First Design Attempt Second Design Attempt Profiling the Second Design Attempt Profiling Example with Visual C++ 2019

1009 1009 1012 1014 1015 1016

Summary 1019 Exercises 1019 CHAPTER 30: BECOMING ADEPT AT TESTING

Quality Control Whose Responsibility Is Testing? The Life Cycle of a Bug Bug-Tracking Tools

Unit Testing Approaches to Unit Testing The Unit Testing Process Define the Granularity of Your Tests Brainstorm the Individual Tests Create Sample Data and Results Write the Tests Run the Tests Unit Testing in Action Introducing the Microsoft Visual C++ Testing Framework Writing the First Test Building and Running Tests Negative Tests Adding the Real Tests

xlii

1021

1022 1022 1022 1023

1025 1026 1026 1027 1028 1029 1029 1030 1031 1031 1033 1034 1034 1035

Contents

Debugging Tests Basking in the Glorious Light of Unit Test Results

Fuzz Testing Higher-Level Testing Integration Tests Sample Integration Tests Methods of Integration Testing System Tests Regression Tests

1038 1038

1039 1039 1039 1039 1040 1041 1041

Tips for Successful Testing 1042 Summary 1043 Exercises 1043 CHAPTER 31: CONQUERING DEBUGGING

The Fundamental Law of Debugging Bug Taxonomies Avoiding Bugs Planning for Bugs

1045

1046 1046 1046 1047

Error Logging 1047 Debug Traces 1049 Debug Mode 1049 Ring Buffers 1053 Assertions 1057 Crash Dumps 1058

Debugging Techniques Reproducing Bugs Debugging Reproducible Bugs Debugging Nonreproducible Bugs Debugging Regressions Debugging Memory Problems Categories of Memory Errors Tips for Debugging Memory Errors Debugging Multithreaded Programs Debugging Example: Article Citations Buggy Implementation of an ArticleCitations Class Testing the ArticleCitations Class Lessons from the ArticleCitations Example

1059 1059 1060 1060 1061 1062 1062 1065 1066 1067 1067 1070 1079

Summary 1079 Exercises 1080

xliii

Contents

CHAPTER 32: INCORPORATING DESIGN TECHNIQUES AND FRAMEWORKS 1083

“I Can Never Remember How to. . .”

1084

. . .Write a Class . . .Derive from an Existing Class . . .Write a Lambda Expression . . .Use the Copy-and-Swap Idiom . . .Throw and Catch Exceptions . . .Write to a File . . .Read from a File . . .Write a Class Template . . .Constrain Template Parameters

1084 1086 1086 1087 1088 1089 1089 1090 1090

There Must Be a Better Way Resource Acquisition Is Initialization Double Dispatch Attempt #1: Brute Force Attempt #2: Single Polymorphism with Overloading Attempt #3: Double Dispatch Mixin Classes Using Multiple Inheritance Using Class Templates

Object-Oriented Frameworks Working with Frameworks The Model-View-Controller Paradigm

1091 1091 1093 1094 1095 1096 1098 1098 1100

1101 1101 1102

Summary 1103 Exercises 1103 CHAPTER 33: APPLYING DESIGN PATTERNS

Dependency Injection Example: A Logging Mechanism Implementation of a Dependency-Injected Logger Using Dependency Injection

The Abstract Factory Pattern Example: A Car Factory Simulation Implementation of an Abstract Factory Using an Abstract Factory

The Factory Method Pattern Example: A Second Car Factory Simulation Implementation of a Factory Using a Factory

xliv

1105

1106 1106 1106 1108

1109 1109 1110 1111

1112 1112 1114 1115

Contents

Other Types of Factories Other Uses of Factories

The Adapter Pattern Example: Adapting a Logger Class Implementation of an Adapter Using an Adapter

The Proxy Pattern Example: Hiding Network Connectivity Issues Implementation of a Proxy Using a Proxy

The Iterator Pattern The Observer Pattern Example: Exposing Events from Subjects Implementation of an Observable Using an Observer

The Decorator Pattern Example: Defining Styles in Web Pages Implementation of a Decorator Using a Decorator

The Chain of Responsibility Pattern Example: Event Handling Implementation of a Chain of Responsibility Using a Chain of Responsibility

The Singleton Pattern Example: A Logging Mechanism Implementation of a Singleton Using a Singleton

1117 1117

1118 1118 1119 1120

1120 1121 1121 1122

1123 1124 1124 1124 1125

1126 1127 1127 1128

1129 1129 1129 1131

1132 1132 1133 1135

Summary 1135 Exercises 1135 CHAPTER 34: DEVELOPING CROSS-PLATFORM AND CROSSLANGUAGE APPLICATIONS

Cross-Platform Development Architecture Issues Size of Integers Binary Compatibility Address Sizes Byte Order Implementation Issues Compiler Quirks and Extensions

1137

1138 1138 1138 1139 1140 1140 1142 1142

xlv

Contents

Library Implementations Handling Different Implementations Platform-Specific Features

Cross-Language Development Mixing C and C++ Shifting Paradigms Linking with C Code Calling C++ Code from C# C++/CLI to Use C# Code from C++ and C++ from C# Calling C++ Code from Java with JNI Calling Scripts from C++ Code Calling C++ Code from Scripts A Practical Example: Encrypting Passwords Calling Assembly Code from C++

1142 1143 1143

1145 1145 1145 1149 1151 1152 1154 1156 1156 1157 1159

Summary 1160 Exercises 1160 PART VI: APPENDICES

xlvi

APPENDIX A: C++ INTERVIEWS

1165

APPENDIX B: ANNOTATED BIBLIOGRAPHY

1191

APPENDIX C: STANDARD LIBRARY HEADER FILES

1203

APPENDIX D: INTRODUCTION TO UML

1213

INDEX

1219

INTRODUCTION

The development of C++ started in 1982 by Bjarne Stroustrup, a Danish computer scientist, as the successor of C with Classes. In 1985, the first edition of The C++ Programming Language book was released. The first standardized version of C++ was released in 1998, called C++98. In 2003, C++03 came out and contained a few small updates. After that, it was silent for a while, but traction slowly started building up, resulting in a major update of the language in 2011, called C++11. From then on, the C++ Standard Committee has been on a three-year cycle to release updated versions, giving us C++14, C++17, and now C++20. All in all, with the release of C++20 in 2020, C++ is almost 40 years old and still going strong. In most rankings of programming languages in 2020, C++ is in the top four. It is being used on an extremely wide range of hardware, going from small devices with embedded microprocessors all the way up to multirack supercomputers. Besides wide hardware support, C++ can be used to tackle almost any programming job, be it games on mobile platforms, performance-critical artificial intelligence (AI) and machine learning (ML) software, real-time 3-D graphics engines, low-level hardware drivers, entire operating systems, and so on. The performance of C++ programs is hard to match with any other programming language, and as such, it is the de facto language for writing fast, powerful, and enterprise-class object-oriented programs. As popular as C++ has become, the language is surprisingly difficult to grasp in full. There are simple, but powerful, techniques that professional C++ programmers use that don’t show up in traditional texts, and there are useful parts of C++ that remain a mystery even to experienced C++ programmers. Too often, programming books focus on the syntax of the language instead of its real-world use. The typical C++ text introduces a major part of the language in each chapter, explaining the syntax and providing an example. Professional C++ does not follow this pattern. Instead of giving you just the nuts and bolts of the language with little practical context, this book will teach you how to use C++ in the real world. It will show you the little-known features that will make your life easier, as well as the programming techniques that separate novices from professional programmers.

WHO THIS BOOK IS FOR Even if you have used the language for years, you might still be unfamiliar with the more advanced features of C++, or you might not be using the full capabilities of the language. Perhaps you write competent C++ code, but would like to learn more about design and good programming style in C++. Or maybe you’re relatively new to C++ but want to learn the “right” way to program from the start. This book will meet those needs and bring your C++ skills to the professional level. Because this book focuses on advancing from basic or intermediate knowledge of C++ to becoming a professional C++ programmer, it assumes that you have some knowledge about programming. Chapter 1, “A Crash Course in C++ and the Standard Library,” covers the basics of C++ as a refresher, but it is not a substitute for actual training in programming. If you are just starting with C++ but you

INTRODUCTION

have significant experience in another programming language such as C, Java, or C#, you should be able to pick up most of what you need from Chapter 1. In any case, you should have a solid foundation in programming fundamentals. You should know about loops, functions, and variables. You should know how to structure a program, and you should be familiar with fundamental techniques such as recursion. You should have some knowledge of common data structures such as queues, and useful algorithms such as sorting and searching. You don’t need to know about object-oriented programming just yet—that is covered in Chapter 5, “Designing with Objects.” You will also need to be familiar with the compiler you will be using to compile your code. Two compilers, Microsoft Visual C++ and GCC, are introduced later in this introduction. For other compilers, refer to the documentation that came with your compiler.

WHAT THIS BOOK COVERS Professional C++ uses an approach to C++ programming that will both increase the quality of your code and improve your programming efficiency. You will find discussions on new C++20 features throughout this fifth edition. These features are not just isolated to a few chapters or sections; instead, examples have been updated to use new features when appropriate. Professional C++ teaches you more than just the syntax and language features of C++. It also emphasizes programming methodologies, reusable design patterns, and good programming style. The Professional C++ methodology incorporates the entire software development process, from designing and writing code to debugging and working in groups. This approach will enable you to master the C++ language and its idiosyncrasies, as well as take advantage of its powerful capabilities for largescale software development. Imagine users who have learned all of the syntax of C++ without seeing a single example of its use. They know just enough to be dangerous! Without examples, they might assume that all code should go in the main() function of the program or that all variables should be global—practices that are generally not considered hallmarks of good programming. Professional C++ programmers understand the correct way to use the language, in addition to the syntax. They recognize the importance of good design, the theories of object-oriented programming, and the best ways to use existing libraries. They have also developed an arsenal of useful code and reusable ideas. By reading and understanding this book, you will become a professional C++ programmer. You will expand your knowledge of C++ to cover lesser known and often misunderstood language features. You will gain an appreciation for object-oriented design and acquire top-notch debugging skills. Perhaps most important, you will finish this book armed with a wealth of reusable ideas that you can actually apply to your daily work. There are many good reasons to make the effort to be a professional C++ programmer as opposed to a programmer who knows C++. Understanding the true workings of the language will improve the quality of your code. Learning about different programming methodologies and processes will xlviii

INTRODUCTION

help you to work better with your team. Discovering reusable libraries and common design patterns will improve your daily efficiency and help you stop reinventing the wheel. All of these lessons will make you a better programmer and a more valuable employee. While this book can’t guarantee you a promotion, it certainly won’t hurt.

HOW THIS BOOK IS STRUCTURED This book is made up of five parts. Part I, “Introduction to Professional C++,” begins with a crash course in C++ basics to ensure a foundation of C++ knowledge. Following the crash course, Part I goes deeper into working with strings, because strings are used extensively in most examples throughout the book. The last chapter of Part I explores how to write readable C++ code. Part II, “Professional C++ Software Design,” discusses C++ design methodologies. You will read about the importance of design, the object-oriented methodology, and the importance of code reuse. Part III, “C++ Coding the Professional Way,” provides a technical tour of C++ from the professional point of view. You will read about the best ways to manage memory in C++, how to create reusable classes, and how to leverage important language features such as inheritance. You will also learn techniques for input and output, error handling, string localization, how to work with regular expressions, and how to structure your code in reusable components called modules. You will read about how to implement operator overloading, how to write templates, how to put restrictions on template parameters using concepts, and how to unlock the power of lambda expressions and function objects. This part also explains the C++ Standard Library, including containers, iterators, ranges, and algorithms. You will also read about some additional libraries that are available in the standard, such as the libraries to work with time, dates, time zones, random numbers, and the filesystem. Part IV, “Mastering Advanced Features of C++,” demonstrates how you can get the most out of C++. This part of the book exposes the mysteries of C++ and describes how to use some of its more advanced features. You will read about how to customize and extend the C++ Standard Library to your needs, advanced details on template programming, including template metaprogramming, and how to use multithreading to take advantage of multiprocessor and multicore systems. Part V, “C++ Software Engineering,” focuses on writing enterprise-quality software. You’ll read about the engineering practices being used by programming organizations today; how to write efficient C++ code; software testing concepts, such as unit testing and regression testing; techniques used to debug C++ programs; how to incorporate design techniques, frameworks, and conceptual object-oriented design patterns into your own code; and solutions for cross-language and cross-platform code. The book concludes with a useful chapter-by-chapter guide to succeeding in a C++ technical interview, an annotated bibliography, a summary of the C++ header files available in the standard, and a brief introduction to the Unified Modeling Language (UML). This book is not a reference of every single class, method, and function available in C++. The book C++17 Standard Library Quick Reference by Peter Van Weert and Marc Gregoire (Apress, 2019.

xlix

INTRODUCTION

ISBN: 978-1-4842-4923-9) is a condensed reference to all essential data structures, algorithms, and functions provided by the C++ Standard Library up until the C++17 standard. Appendix B lists a couple more references. Two excellent online references are: ➤

cppreference.com: You can use this reference online or download an offline version for use

when you are not connected to the Internet. ➤

cplusplus.com/reference/

When I refer to a “Standard Library Reference” in this book, I am referring to one of these detailed C++ references. The following are additional excellent online resources: ➤

github.com/isocpp/CppCoreGuidelines: The C++ Core Guidelines are a collaborative

effort led by Bjarne Stroustrup, inventor of the C++ language itself. They are the result of many person-years of discussion and design across a number of organizations. The aim of the guidelines is to help people to use modern C++ effectively. The guidelines are focused on relatively higher-level issues, such as interfaces, resource management, memory management, and concurrency. ➤

github.com/Microsoft/GSL: This is an implementation by Microsoft of the Guidelines

Support Library (GSL) containing functions and types that are suggested for use by the C++ Core Guidelines. It’s a header-only library. ➤

isocpp.org/faq: This is a large collection of frequently asked C++ questions.



stackoverflow.com: Search for answers to common programming questions, or ask your

own questions.

CONVENTIONS To help you get the most from the text and keep track of what’s happening, a number of conventions are used throughout this book.

WARNING Boxes like this one hold important, not-to-be-forgotten information

that is directly relevant to the surrounding text.

NOTE Tips, hints, tricks, and asides to the current discussion are placed in boxes

like this one.

l

INTRODUCTION

As for styles in the text: Important words are italic when they are introduced. Keyboard strokes are shown like this: Ctrl+A. Filenames and code within the text are shown like so: monkey.cpp. URLs are shown like this: wrox.com. Code is presented in three different ways: // Comments in code are shown like this. In code examples, new and important code is highlighted like this. Code that's less important in the present context or that has been shown before is formatted like this. C++20

Paragraphs or sections that are specific to the C++20 standard have a little C++20 icon on the left, just as this paragraph does. C++11, C++14, and C++17 features are not marked with any icon.

WHAT YOU NEED TO USE THIS BOOK All you need to use this book is a computer with a C++ compiler. This book focuses only on parts of C++ that have been standardized, and not on vendor-specific compiler extensions.

Any C++ Compiler You can use whichever C++ compiler you like. If you don’t have a C++ compiler yet, you can download one for free. There are a lot of choices. For example, for Windows, you can download Microsoft Visual Studio Community Edition, which is free and includes Visual C++. For Linux, you can use GCC or Clang, which are also free. The following two sections briefly explain how to use Visual C++ and GCC. Refer to the documentation that came with your compiler for more details.

COMPILERS AND C++20 FEATURE SUPPORT This book discusses new features introduced with the C++20 standard. At the time of this writing, no compilers were fully C++20 compliant yet. Some new features were only supported by some compilers and not others, while other features were not yet supported by any compiler. Compiler vendors are hard at work to catch up with all new features, and I’m sure it won’t take long before there will be fully C++20-compliant compilers available. You can keep track of which compiler supports which features at en.cppreference.com/w/cpp/compiler_support.

li

INTRODUCTION

COMPILERS AND C++20 MODULE SUPPORT At the time of this writing, there was no compiler available yet that fully supported C++20 modules. There was experimental support in some of the compilers, but it was still incomplete. This book uses modules everywhere. We did our best to make sure all sample code would compile once compilers fully support modules, but since we were not able to compile and test all examples, some errors might have crept in. When you use a compiler with support for modules and you encounter problems with any of the code samples, double-check the list of errata for the book at www .wiley.com/go/proc++5e to see if it’s a known issue. If your compiler does not yet support modules, you can convert modularized code to non-modularized code, as explained briefly in Chapter 11, “Odds and Ends.”

Example: Microsoft Visual C++ 2019 First, you need to create a project. Start Visual C++ 2019, and on the welcome screen, click the Create A New Project button. If the welcome screen is not shown, select File ➪ New ➪ Project. In the Create A New Project dialog, search for the Console App project template with tags C++, Windows, and Console, and click Next. Specify a name for the project and a location where to save it, and click Create. Once your new project is loaded, you can see a list of project files in the Solution Explorer. If this docking window is not visible, select View ➪ Solution Explorer. A newly created project will contain a file called .cpp. You can start writing your C++ code in that .cpp file, or if you want to compile source code files from the downloadable source archive for this book, select the .cpp file in the Solution Explorer and delete it. You can add new files or existing files to a project by right-clicking the project name in the Solution Explorer and then selecting Add ➪ New Item or Add ➪ Existing Item. At the time of this writing, Visual C++ 2019 did not yet automatically enable C++20 features. To enable C++20 features, in the Solution Explorer window, right-click your project and click Properties. In the Properties window, go to Configuration Properties ➪ C/C++ ➪ Language, and set the C++ Language Standard option to ISO C++20 Standard or Preview - Features from the Latest C++ Working Draft, whichever is available in your version of Visual C++. These options are accessible only if your project contains at least one .cpp file. Finally, select Build ➪ Build Solution to compile your code. When it compiles without errors, you can run it with Debug ➪ Start Debugging.

Module Support At the time of this writing, Visual C++ 2019 did not yet have full support for modules. Authoring and consuming your own modules usually works just fine, but importing Standard Library headers such as the following did not yet work out of the box: import ; lii

INTRODUCTION

To make such import declarations work, for the time being you need to add a separate header file to your project, for example called HeaderUnits.h, which contains an import declaration for every Standard Library header you want to import. Here’s an example: // HeaderUnits.h #pragma once import ; import ; import ; import ; // ...

Next, right-click the HeaderUnits.h file in the Solution Explorer and click Properties. In Configuration Properties ➪ General, set Item Type to C/C++ Compiler and click Apply. Next, in Configuration Properties ➪ C/C++ ➪ Advanced, set Compile As to Compile as C++ Header Unit (/exportHeader) and click OK. When you now recompile your project, all import declarations that have a corresponding import declaration in your HeaderUnits.h file should compile fine. If you are using module implementation partitions (see Chapter 11), also known as internal partitions, then right-click all files containing such implementation partitions, click Properties, go to Configuration Properties ➪ C/C++ ➪ Advanced, and set the Compile As option to Compile as C++ Module Internal Partition (/internalPartition) and click OK.

Example: GCC Create your source code files with any text editor you prefer and save them to a directory. To compile your code, open a terminal and run the following command, specifying all your .cpp files that you want to compile: g++ -std=c++2a -o [source2.cpp ...]

The -std=c++2a option is required to tell GCC to enable C++20 support. This option will change to -std=C++20 once GCC is fully C++20 compliant.

Module Support At the time of this writing, GCC only had experimental support for modules through a special version of GCC (branch devel/c++-modules). When you are using such a version of GCC, module support is enabled with the -fmodules-ts option, which might change to -fmodules in the future. Unfortunately, import declarations of Standard Library headers such as the following were not yet properly supported: import ;

If that’s the case, simply replace such import declarations with corresponding #include directives: #include

liii

INTRODUCTION

For example, the AirlineTicket example from Chapter 1 uses modules. After having replaced the imports for Standard Library headers with #include directives, you can compile the AirlineTicket example by changing to the directory containing the code and running the following command: g++ -std=c++2a -fmodules-ts -o AirlineTicket AirlineTicket.cppm AirlineTicket.cpp AirlineTicketTest.cpp

When it compiles without errors, you can run it as follows: ./AirlineTicket

std::format Support Many code samples in this book use std::format(), introduced in Chapter 1. At the time of this writing, there was no compiler yet that had support for std::format(). However, as long as your compiler doesn’t support std::format() yet, you can use the freely available {fmt} library as a drop-in replacement:

1.

Download the latest version of the {fmt} library from https://fmt.dev/ and extract the code on your machine.

2.

Copy the include/fmt and src directories to fmt and src subdirectories in your project directory, and then add fmt/core.h, fmt/format.h, fmt/format-inl.h, and src/format .cc to your project.

3.

Add a file called format (no extension) to the root directory of your project and add the following code to it: #pragma once #define FMT_HEADER_ONLY #include "fmt/format.h" namespace std { using fmt::format; using fmt::format_error; using fmt::formatter; }

4.

Finally, add your project root directory (the directory containing the format file) as an additional include directory for your project. For example, in Visual C++, right click your project in the Solution Explorer, click Properties, go to Configuration Properties ➪ C/C++ ➪ General, and add $(ProjectDir); to the front of the Additional Include Directories option.

NOTE Don’t forget to undo these steps once your compiler supports the standard std::format().

liv

INTRODUCTION

READER SUPPORT FOR THIS BOOK The following sections describe different options to get support for this book.

Companion Download Files As you work through the examples in this book, you may choose either to type in all the code manually or to use the source code files that accompany the book. However, I suggest you type in all the code manually because it greatly benefits the learning process and your memory. All of the source code used in this book is available for download at www.wiley.com/go/proc++5e.

NOTE Because many books have similar titles, you may find it easiest to search by

ISBN; for this book, the ISBN is 978-1-119-69540-0.

Once you’ve downloaded the code, just decompress it with your favorite decompression tool.

How to Contact the Publisher If you believe you’ve found a mistake in this book, please bring it to our attention. At John Wiley & Sons, we understand how important it is to provide our customers with accurate content, but even with our best efforts an error may occur. To submit your possible errata, please e-mail it to our Customer Service Team at wileysupport@ wiley.com with “Possible Book Errata Submission” as a subject line.

How to Contact the Author If you have any questions while reading this book, the author can easily be reached at [email protected] and will try to get back to you in a timely manner.

lv

PART I

Introduction to Professional C++ ▸▸ CHAPTER 1: A Crash Course in C++ and the Standard Library ▸▸ CHAPTER 2: Working with Strings and String Views ▸▸ CHAPTER 3: Coding with Style

Professional C++, Fifth Edition. Marc Gregoire. © 2021 John Wiley & Sons, Inc. Published 2021 by John Wiley & Sons, Inc.

1

A Crash Course in C++ and the Standard Library WHAT’S IN THIS CHAPTER? ➤➤

A brief overview of the most important parts and syntax of the C++ language and the Standard Library

➤➤

How to write a basic class

➤➤

How scope resolution works

➤➤

What uniform initialization is

➤➤

The use of const

➤➤

What pointers, references, exceptions, and type aliases are

➤➤

Basics of type inference

WILEY.COM DOWNLOADS FOR THIS CHAPTER

Please note that all the code examples for this chapter are available as a part of the chapter’s code download on this book’s website at www.wiley.com/go/proc++5e on the Download Code tab. The goal of this chapter is to cover briefly the most important parts of C++ so that you have a foundation of knowledge before embarking on the rest of this book. This chapter is not a comprehensive lesson in the C++ programming language or the Standard Library. Certain basic points, such as what a program is and what recursion is, are not covered. Esoteric points, such as the definition of a union, or the volatile keyword, are also omitted. Certain parts of the C language that are less relevant in C++ are also left out, as are parts of C++ that get in-depth coverage in later chapters.

Professional C++, Fifth Edition. Marc Gregoire. © 2021 John Wiley & Sons, Inc. Published 2021 by John Wiley & Sons, Inc.

4 

❘  CHAPTER 1   A Crash Course in C++ and the Standard Library

This chapter aims to cover the parts of C++ that programmers encounter every day. For example, if you’re fairly new to C++ and don’t understand what a reference variable is, you’ll learn about that kind of variable here. You’ll also learn the basics of how to use the functionality available in the Standard Library, such as vector containers, optional values, string objects, and more. These parts of the Standard Library are briefly introduced in Chapter 1 so that these modern constructs can be used throughout examples in this book from the beginning. If you already have significant experience with C++, skim this chapter to make sure that there aren’t any fundamental parts of the language on which you need to brush up. If you’re new to C++, read this chapter carefully and make sure you understand the examples. If you need additional introductory information, consult the titles listed in Appendix B.

C++ CRASH COURSE The C++ language is often viewed as a “better C” or a “superset of C.” It was mainly designed to be an object-oriented C, commonly called as “C with classes.” Later on, many of the annoyances and rough edges of the C language were addressed as well. Because C++ is based on C, some of the syntax you’ll see in this section will look familiar to you if you are an experienced C programmer. The two languages certainly have their differences, though. As evidence, The C++ Programming Language by C++ creator Bjarne Stroustrup (fourth edition; Addison-Wesley Professional, 2013) weighs in at 1,368 pages, while Kernighan and Ritchie’s The C Programming Language (second edition; Prentice Hall, 1988) is a scant 274 pages. So, if you’re a C programmer, be on the lookout for new or unfamiliar syntax!

The Obligatory “Hello, World” Program In all its glory, the following code is the simplest C++ program you’re likely to encounter: // helloworld.cpp import ; int main() { std::cout "; cin >> selection; return selection; }

The doHire() function gets the new employee’s name from the user and tells the database to add the employee: void doHire(Database& db) { string firstName; string lastName; cout > firstName; cout > lastName; auto& employee { db.addEmployee(firstName, lastName) }; cout someOtherMethod();

This is flagged as an error by the compiler because, although the object is of type Derived and therefore does have someOtherMethod() defined, the compiler can only think of it as type Base, which does not have someOtherMethod() defined.

A Derived Class’s View of Inheritance To the derived class, nothing much has changed in terms of how it is written or how it behaves. You can still define methods and data members on a derived class just as you would on a regular class. The previous definition of Derived declares a method called someOtherMethod(). Thus, the Derived class augments the Base class by adding an additional method. A derived class can access public and protected methods and data members declared in its base class as though they were its own, because technically they are. For example, the implementation of someOtherMethod() on Derived could make use of the data member m_protectedInt, which is declared as part of Base. The following code shows this. Accessing a base class data member or method is no different than if the data member or method were declared as part of the derived class. void Derived::someOtherMethod() { cout {}\n", value); } cout 6 4 > 8 5 Terminating...

SUMMARY This chapter explained the ideas behind iterators, which are an abstraction that allows you to navigate the elements of a container without the need to know the structure of the container. You have seen that output stream iterators can use standard output as a destination for iterator-based algorithms, and similarly that input stream iterators can use standard input as the source of data for algorithms. The chapter also discussed the insert-, reverse-, and move iterator adapters that can be used to adapt other iterators. The last part of this chapter discussed the ranges library, part of the C++20 Standard Library. It allows you to write more functional-style code, by specifying what you want to accomplish instead of how. You can construct pipelines consisting of a combination of operations applied to the elements of a range. Such pipelines are executed lazily; that is, they don’t do anything until you iterate over the resulting view.

626 

❘  CHAPTER 17   Understanding Iterators and the Ranges Library

EXERCISES By solving the following exercises, you can practice the material discussed in this chapter. Solutions to all exercises are available with the code download on the book’s website at www.wiley.com/go/ proc++5e. However, if you are stuck on an exercise, first reread parts of this chapter to try to find an answer yourself before looking at the solution from the website. Exercise 17-1: Write a program that lazily constructs the sequence of elements 10-100, squares each number, removes all numbers dividable by five, and transforms the remaining values to strings using std::to_string(). Exercise 17-2: Write a program that creates a vector of pairs, where each pair contains an instance of the Person class introduced earlier in this chapter, and their age. Next, use the ranges library to construct a single pipeline that extracts all ages from all persons from the vector, and removes all ages below 12 and above 65. Finally, calculate the average of the remaining ages using the sum() algorithm from earlier in this chapter. As you’ll pass a range to the sum() algorithm, you’ll have to work with a common range. Exercise 17-3: Building further on the solution for Exercise 17-2, add an implementation for

operator doubleVector[i]; if (doubleVector[i] > max) { max = doubleVector[i]; } } max /= 100.0; for (auto& element : doubleVector) { element /= max; cout max) { max = temp; } } max /= 100.0; for (auto& element : doubleVector) { element /= max; cout second; } BankAccount& BankDB::findAccount(string_view name) { // Finding an element by a non-key attribute requires a linear // search through the elements. With C++17 structured bindings: for (auto& [accountNumber, account] : m_accounts) { if (account.getClientName() == name) { return account; // found it! } } throw out_of_range { "No account with that name." }; } void BankDB::mergeDatabase(BankDB& db) { // Use C++17 merge(). m_accounts.merge(db.m_accounts); // Or: m_accounts.insert(begin(db.m_accounts), end(db.m_accounts)); // Now clear the source database. db.m_accounts.clear(); }

You can test the BankDB class with the following code: BankDB db; db.addAccount(BankAccount { 100, "Nicholas Solter" }); db.addAccount(BankAccount { 200, "Scott Kleper" }); try { auto& account { db.findAccount(100) }; cout {}.", tally, value) 3

The example can be extended to demonstrate capturing variables by reference. The following lambda expression counts the number of times it is called by incrementing a variable in the enclosing scope that is captured by reference: vector values { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; int value { 3 }; int callCounter { 0 };

Algorithm Details 

❘  737

auto tally { count_if(cbegin(values), cend(values), [value, &callCounter](int i){ ++callCounter; return i > value; }) }; cout (diff1).count()); cout second; return true; } return false; } // Adds a new name to the database. void NameDB::addNewName(string_view name) { m_names[name.data()] = 1; } int NameDB::getNameRank(string_view name) const { // Implementation omitted, same as before. } // Returns the count associated with the given name. int NameDB::getAbsoluteNumber(string_view name) const { auto res { m_names.find(name.data()) }; if (res != end(m_names)) { return res->second; } return -1; }

Profiling the Second Design Attempt By following the same steps shown earlier, you can obtain the gprof performance data on the new version of the program. The data is quite encouraging: index %time [1] 100.0

[2] [3]

95.2

self 0.00 0.02 0.00 0.00 0.02 0.02 0.00 0.00

children called 0.21 0.18 1/1 0.01 1/1 0.00 3/3 0.18 1 0.16 500500/500500 0.00 1000/1000 0.00 1/1

name main [1] NameDB::NameDB [2] NameDB::~NameDB [13] NameDB::getNameRank [28] NameDB::NameDB [2] NameDB::nameExistsAndIncrement NameDB::addNewName [24] map::map [87]

1016 

❘  CHAPTER 29   Writing Efficient C++

If you run this on your machine, the output will be different. It’s even possible that you will not see any data for NameDB methods in your output. Because of the efficiency of this second attempt, the timings are getting so small that you might see more map methods in the output than NameDB methods. On my test system, main() now takes only 0.21 seconds—a 67-fold improvement! There are certainly further improvements that you could make to this program. For example, the current constructor performs a lookup to see if the name is already in the map, and if not, adds it to the map. You could combine these two operations simply with the following single line: ++m_names[name];

If the name is already in the map, this statement just increments its counter. If the name is not yet in the map, this statement first adds an entry to the map with the given name as key and a zero-initialized value and then increments the value, resulting in a counter of 1. With this improvement, you can remove the nameExistsAndIncrement() and addNewName() methods and change the constructor as follows: NameDB::NameDB(string_view nameFile) { // Open the file and check for errors. ifstream inputFile { nameFile.data() }; if (!inputFile) { throw invalid_argument { "Unable to open file" }; } // Read the names one at a time. string name; while (inputFile >> name) { ++m_names[name]; } } getNameRank() still uses a loop that iterates over all elements in the map. A good exercise for you to try is to come up with another data structure so that the linear iteration in getNameRank() can

be avoided.

Profiling Example with Visual C++ 2019 Most editions of Microsoft Visual C++ 2019 come with a great built-in profiler, which is briefly discussed in this section. The VC++ profiler has a complete graphical user interface. This book does not recommend one profiler over another, but it is always good to have an idea of what a command line– based profiler like gprof can provide in comparison with a GUI-based profiler like the one included with VC++. To start profiling an application in Visual C++ 2019, you first need to open the project in Visual Studio. This example uses the same NameDB code as in the first inefficient design attempt from the previous sections. That code is not repeated here. Once your project is opened in Visual Studio, make sure the configuration is set to Release instead of Debug, then click the Analyze menu, and choose Performance Profiler. A new window appears, as shown in Figure 29‑1.

Profiling 

❘  1017

FIGURE 29-1 

Depending on your version of VC++, there will be a number of different analysis tools available from this window. The following non-exhaustive list explains two of them: ➤➤

CPU Usage: This tool is used to monitor applications with low overhead. This means that the act of profiling the application will not have a big performance impact on the target application.

➤➤

Instrumentation: This tool adds extra code to the application to be able to accurately count the number of function calls and to time individual function calls. However, this tool has a much bigger performance impact on the application. It is recommended to use the CPU Usage tool first to get an idea about the bottlenecks in your application. If that tool does not give you enough information, you can try the Instrumentation tool.

For this profiling example, enable only the CPU Usage tool and click the Start button. This will start executing your program and analyze its CPU usage. When the program execution is finished, Visual Studio automatically opens the profiling report. Figure 29‑2 shows how this report might look like when profiling the first attempt of the NameDB application. From this report, you can immediately see the hot path. Just like with gprof, it shows that the NameDB constructor takes up most of the running time of the program, and that incrementNameCount() and nameExists() both roughly take the same time. The Visual Studio profiling report is interactive. For example, you can drill down the NameDB constructor by double clicking on it. This results in a drilldown report for that function, as show in Figure 29‑3.

1018 

❘  CHAPTER 29   Writing Efficient C++

FIGURE 29-2 

FIGURE 29-3 

This drill-down view shows a graphical breakdown at the top and the actual code of the method at the bottom. The code view shows the percentage of the running time that a line of code needed. The lines using up most of the time are shown in shades of red.

Exercises 

❘  1019

At the top of this report, there is a drop-down called Current View. Selecting Call Tree from that drop-down shows an alternative view of the hot path in your code. Once in the Call Tree view, click the Expand Hot Path button to expand the hot path in your code, as shown in Figure 29‑4.

FIGURE 29-4 

Also in this view, you immediately see that main() is calling the NameDB constructor, which is using up most of the running time.

SUMMARY This chapter discussed the key aspects of efficiency and performance in C++ programs and provided several specific tips and techniques for designing and writing more efficient applications. Ideally, you gained an appreciation for the importance of performance and for the power of profiling tools. There are two important things to remember from this chapter. The first thing is that you should not get too obsessed with performance while designing and coding. It’s recommended to first make a correct, well-structured design and implementation, then use a profiler, and only optimize those parts that are flagged by a profiler as being a performance bottleneck. The second and most important thing to remember from this chapter is that design-level efficiency is far more important than language-level efficiency. For example, you shouldn’t use algorithms or data structures with bad complexity if there are better ones available.

EXERCISES By solving the following exercises, you can practice the material discussed in this chapter. Solutions to all exercises are available with the code download on the book’s website at www.wiley.com/go/ proc++5e. However, if you are stuck on an exercise, first reread parts of this chapter to try to find an answer yourself before looking at the solution from the website. Exercise 29-1: Which efficiency problems can you spot in the following code snippet?

1020 

❘  CHAPTER 29   Writing Efficient C++

class Bar { }; class Foo { public: explicit Foo(Bar b) {} }; Foo getFoo(bool condition, Bar b1, Bar b2) { return condition ? Foo { b1 } : Foo { b2 }; } int main() { Bar b1, b2; auto foo { getFoo(true, b1, b2) }; }

Exercise 29-2: What are the two most important things to remember from this chapter? Exercise 29-3: Modify the final NameDB solution from the “Profiling” section to use an std::unordered_map instead of a map. Profile your code before and after your changes and compare the results. Exercise 29-4: From the profiling results of Exercise 29-3, it now looks like operator>> in the NameDB constructor is the bottleneck. Can you change the implementation to avoid using operator>>? Since each line in the input file contains one name, maybe it’s faster to simply read names line by line? Try to modify your implementation as such and compare the profiling results before and after your changes.

30

Becoming Adept at Testing WHAT’S IN THIS CHAPTER? ➤➤

What software quality control is and how to track bugs

➤➤

What unit testing means

➤➤

Unit testing in practice using the Visual C++ Testing Framework

➤➤

What fuzz testing or fuzzing means

➤➤

What integration, system, and regression testing means

WILEY.COM DOWNLOADS FOR THIS CHAPTER

Please note that all the code examples for this chapter are available as part of this chapter’s code download on the book’s website at www.wiley.com/go/proc++5e on the Download Code tab. A programmer has overcome a major hurdle in her career when she realizes that testing is part of the software development process. Bugs are not an occasional occurrence. They are found in every project of significant size. A good quality assurance (QA) team is invaluable, but the full burden of testing cannot be placed on QA alone. Your responsibility as a programmer is to write code that works and to write tests to prove its correctness. A distinction is often made between white-box testing, in which the tester is aware of the inner workings of the program, and black-box testing, which tests the program’s functionality without any knowledge of its implementation. Both forms of testing are important to professional-quality projects. Black-box testing is the most fundamental approach because it typically models the behavior of a user. For example, a black-box test can examine interface components such as buttons. If the tester clicks the button and nothing happens, there is obviously a bug in the program.

Professional C++, Fifth Edition. Marc Gregoire. © 2021 John Wiley & Sons, Inc. Published 2021 by John Wiley & Sons, Inc.

1022 

❘  CHAPTER 30   Becoming Adept at Testing

Black-box testing cannot cover everything. Modern programs are too large to employ a simulation of clicking every button, providing every kind of input, and performing all combinations of commands. White-box testing is necessary, because when you know the code when tests are written at the object or subsystem level, then it is easier to make sure all code paths in the code are exercised by tests. This helps to ensure test coverage. White-box tests are often easier to write and automate than black-box tests. This chapter focuses on topics that would generally be considered white-box testing techniques because the programmer can use these techniques during the development. This chapter begins with a high-level discussion of quality control, including some approaches to viewing and tracking bugs. A section on unit testing, one of the simplest and most useful types of testing, follows this introduction. You then read about the theory and practice of unit testing, as well as several examples of unit tests in action. Next, higher-level tests are covered, including integration tests, system tests, and regression tests. Finally, this chapter ends with a list of tips for successful testing.

QUALITY CONTROL Large programming projects are rarely finished when a feature-complete goal is reached. There are always bugs to find and fix, both during and after the main development phase. It is essential to understand the shared responsibility of quality control and the life cycle of a bug to perform well in a group.

Whose Responsibility Is Testing? Software development organizations have different approaches to testing. In a small startup, there may not be a group of people whose full-time job is testing the product. Testing may be the responsibility of the individual developers, or all the employees of the company may be asked to lend a hand and try to break the product before its release. In larger organizations, a full-time quality assurance staff probably qualifies a release by testing it according to a set of criteria. Nonetheless, some aspects of testing may still be the responsibility of the developers. Even in organizations where the developers have no role in formal testing, you still need to be aware of what your responsibilities are in the larger process of quality assurance.

The Life Cycle of a Bug All good engineering groups recognize that bugs will occur in software both before and after its release. There are many different ways to deal with these problems. Figure 30‑1 shows a formal bug process, expressed as a flow chart. In this particular process, a bug is always filed by a member of the QA team. The bug reporting software sends a notification to the development manager, who sets the priority of the bug and assigns the bug to the appropriate module owner. The module owner can accept the bug or explain why the bug actually belongs to a different module or is invalid, giving the development manager the opportunity to assign it to someone else. Once the bug has found its rightful owner, a fix is made, and the developer marks the bug as “fixed.” At this point, the QA engineer verifies that the bug no longer exists and marks the bug as “closed” or reopens the bug if it is still present.

Quality Control 

Received by Manager

Rejected

Priority Assigned

Received by Module Owner

Accept or Reject

Bug Report Filed by QA

❘  1023

Received by QA Engineer

Re-opened Check Fix

Accepted

Owner Determined

Bug Fixed

Verified Bug Closed

FIGURE 30-1 

Figure 30‑2 shows a less formal approach. In this workflow, anybody can file a bug and assign an initial priority and a module. The module owner receives the bug report and can either accept it or reassign it to another engineer or module. When a correction is made, the bug is marked as “fixed.” Toward the end of the testing phase, all the developers and QA engineers divide up the fixed bugs and verify that each bug is no longer present in the current build. The release is ready when all bugs are marked as “closed.” Received by Module Owner

Bug Report Filed with Priority and Module

Reassigned

Accept or Reject Accepted Bug Fixed

FIGURE 30-2 

Bug-Tracking Tools There are many ways to keep track of software bugs, from informal spreadsheet- or e-mail-based schemes to expensive third-party bug-tracking software. The appropriate solution for your organization depends on the group’s size, the nature of the software, and the level of formality you want to build around bug fixing.

1024 

❘  CHAPTER 30   Becoming Adept at Testing

There are also a number of free open-source bug-tracking solutions available. One of the more popular free tools for bug tracking is Bugzilla (bugzilla.org), written by the authors of the Mozilla and Firefox web browser. As an open-source project, Bugzilla has gradually accumulated a number of useful features to the point where it now rivals expensive bug-tracking software packages. Here are just a few of its many features: ➤➤

Customizable settings for a bug, including its priority, associated component, status, and so on

➤➤

E-mail notification of new bug reports or changes to an existing report

➤➤

Tracking of dependencies between bugs and resolution of duplicate bugs

➤➤

Reporting and searching tools

➤➤

A web-based interface for filing and updating bugs

Figure 30‑3 shows a bug being entered into a Bugzilla project that was set up for the second edition of this book. For my purposes, each chapter was input as a Bugzilla component. The filer of the bug can specify the severity of the bug (how big of a deal it is). A summary and description are included to make it possible to search for the bug or list it in a report format.

FIGURE 30-3 

Unit Testing 

❘  1025

Bug-tracking tools like Bugzilla are essential components of a professional software development environment. In addition to supplying a central list of currently open bugs, bug-tracking tools provide an important archive of previous bugs and their fixes. A support engineer, for instance, might use the tool to search for a problem similar to one reported by a customer. If a fix was made, the support person will be able to tell the customer which version they need to update to or how to work around the problem.

UNIT TESTING The only way to find bugs is through testing. One of the most important types of tests from a developer’s point of view is the unit test. Unit tests are pieces of code that exercise specific functionality of a class or subsystem. These are the finest-grained tests that you could possibly write. Ideally, one or more unit tests should exist for every low-level task that your code can perform. For example, imagine that you are writing a math library that can perform addition and multiplication. Your suite of unit tests might contain the following tests: ➤➤

Test a simple addition

➤➤

Test addition of large numbers

➤➤

Test addition of negative numbers

➤➤

Test addition of zero to a number

➤➤

Test the commutative property of addition

➤➤

Test a simple multiplication

➤➤

Test multiplication of large numbers

➤➤

Test multiplication of negative numbers

➤➤

Test multiplication with zero

➤➤

Test the commutative property of multiplication

Well-written unit tests protect you in many ways: ➤➤

They prove that a piece of functionality actually works. Until you have some code that actually makes use of your class, its behavior is a major unknown.

➤➤

They provide a first alert when a recently introduced change breaks something. This specific usage, called a regression test, is covered later in this chapter.

➤➤

When used as part of the development process, they force the developer to fix problems from the start. If you are prevented from checking in your code with failed unit tests, then you’re forced to address problems right away.

1026 

❘  CHAPTER 30   Becoming Adept at Testing

➤➤

Unit tests let you try code before other code is in place. When you first started programming, you could write an entire program and then run it for the first time. Professional programs are too big for that approach, so you need to be able to test components in isolation.

➤➤

Last, but certainly not least, they provide an example of usage. Almost as a side effect, unit tests make great reference code for other programmers. If a co-worker wants to know how to perform matrix multiplication by using your math library, you can point her to the appropriate test.

Approaches to Unit Testing It’s hard to go wrong with unit tests, unless you don’t write them or you write them poorly. In general, the more tests you have, the more coverage you have. The more coverage you have, the less likely it is for bugs to fall through the cracks and for you to have to tell your boss, or worse, your customer, “Oh, we never tested that.” There are several methodologies for writing unit tests most effectively. The eXtreme Programming methodology, explained in Chapter 28, “Maximizing Software Engineering Methods,” instructs its followers to write unit tests before writing code. Writing tests first helps you to solidify the requirements for the component and to provide a metric that can be used to determine when coding is done. However, writing tests first can be tricky and requires diligence on the part of the programmer. For some programmers, it simply doesn’t mesh well with their coding style. A less rigid approach is to design the tests before coding but implement them later in the process. This way, the programmer is still forced to understand the requirements of the module but doesn’t have to write code that makes use of nonexistent classes. In some groups, the author of a particular subsystem doesn’t write the unit tests for that subsystem. The idea is that if you write the tests for your own code, you might subconsciously work around problems that you know about or only cover certain cases that you know your code handles well. In addition, it’s sometimes difficult to get excited about finding bugs in code you just wrote, so you might only put in a half-hearted effort. Having one developer write unit tests for another developer’s code requires a lot of extra overhead and coordination. When such coordination is accomplished, however, this approach helps guarantee more effective tests. Another way to ensure that unit tests are actually testing the right parts of the code is to write them so that they maximize code coverage. You can use a code coverage tool, such as gcov (gcc.gnu.org/ onlinedocs/gcc/Gcov.html), that tells you what percentage of the code is called by unit tests. The idea is that a properly tested piece of code has unit tests to test all possible code paths that can be taken through that piece of code.

The Unit Testing Process The process of providing unit tests for your code starts from the beginning, long before any code is written. Keeping unit testability in mind during the design phase can influence the design decisions you make for your software. Even if you do not subscribe to the methodology of writing unit tests before you write code, you should at least take the time to consider what sorts of tests you will provide, even while still in the design phase. This way, you can break the task up into well-defined chunks, each of which has its own test-validated criteria. For example, if your task is to write a database access class, you might first write the functionality that inserts data into the database. Once that

Unit Testing 

❘  1027

is fully tested with a suite of unit tests, you can continue to write the code to support updates, deletes, and selects, testing each piece as you go. The following list of steps is a suggested approach for designing and implementing unit tests. As with any programming methodology, the best process is the one that yields the best results. I suggest that you experiment with different ways of using unit tests to discover what works best for you.

Define the Granularity of Your Tests Writing unit tests takes time; there is no way around this. Software developers are often crunched for time. To reach deadlines, developers tend to skip writing unit tests, because they think they will finish faster that way. Unfortunately, this thinking does not take the whole picture into account. Omitting unit tests will backfire in the long run. The earlier a bug is detected in the software development process, the less it costs. If a developer finds a bug during unit testing, it can be fixed immediately, before anyone else encounters it. However, if the bug is discovered by QA, then it becomes a much costlier bug. The bug can cause an extra development cycle, requiring bug management; it has to go back to the development team for a fix and then back to QA to verify the fix. If a bug slips through the QA process and finds its way to the customer, then it becomes even more expensive. The granularity of tests refers to their scope. As the following table illustrates, you can initially unit test a database class with just a few test functions and then gradually add more tests to ensure that everything works as it should: LARGE-GRAINED

MEDIUM-GRAINED TESTS

FINE-GRAINED TESTS

TESTS

testConnection()

testConnectionDropped()

testConnectionThroughHTTP()

testInsert()

testInsertBadData()

testConnectionLocal()

testUpdate()

testInsertStrings()

testConnectionErrorBadHost()

testDelete()

testInsertIntegers()

testConnectionErrorServerBusy()

testSelect()

testUpdateStrings()

testInsertWideCharacters()

testUpdateIntegers()

testInsertLargeData()

testDeleteNonexistentRow()

testInsertMalformed()

testSelectComplicated()

testUpdateWideCharacters()

testSelectMalformed()

testUpdateLargeData() testUpdateMalformed() testDeleteWithoutPermissions() testDeleteThenUpdate() testSelectNested() testSelectWideCharacters() testSelectLargeData()

As you can see, each successive column brings in more-specific tests. As you move from large-grained tests to more finely grained tests, you start to consider error conditions, different input data sets, and different modes of operation.

1028 

❘  CHAPTER 30   Becoming Adept at Testing

Of course, the decisions you make initially when choosing the granularity of your tests are not set in stone. Perhaps the database class is just being written as a proof of concept and might not even be used. A few simple tests may be adequate now, and you can always add more later. Or perhaps the use cases will change at a later date. For example, the database class might not initially have been written with international characters in mind. Once such features are added, they should be tested with specific targeted unit tests. Consider the unit tests to be part of the actual implementation of a feature. When you make a modification, don’t just modify the tests so that they continue to work. Write new tests and re-evaluate the existing ones. When bugs are uncovered and fixed, add new unit tests that specifically test those fixes.

WARNING  Unit tests are part of the subsystem that they are testing. As you enhance and refine the subsystem, enhance and refine the tests.

Brainstorm the Individual Tests Over time, you will gain an intuition for which aspects of a piece of code should turn into a unit test. Certain methods or inputs will just feel like they should be tested. This intuition is gained through trial and error and by looking at unit tests that other people in your group have written. It should be pretty easy to pick out which programmers are the best unit testers. Their tests tend to be organized and frequently modified. Until unit test creation becomes second nature, approach the task of figuring out which tests to write by brainstorming. To get some ideas flowing, consider the following questions: ➤➤

What are the things that this piece of code was written to do?

➤➤

What are the typical ways each method would be called?

➤➤

What preconditions of the methods could be violated by the caller?

➤➤

How could each method be misused?

➤➤

What kinds of data are you expecting as input?

➤➤

What kinds of data are you not expecting as input?

➤➤

What are the edge cases or exceptional conditions?

You don’t need to write formal answers to those questions (unless your manager is a particularly fervent devotee of this book or of certain testing methodologies), but they should help you generate some ideas for unit tests. The table of tests for the database class contained test functions, each of which arose from one of these questions. Once you have generated ideas for some of the tests you would like to use, consider how you might organize them into categories; the breakdown of tests will fall into place. In the database class example, the tests could be split into the following categories: ➤➤

Basic tests

➤➤

Error tests

Unit Testing 

➤➤

Localization tests

➤➤

Bad input tests

➤➤

Complicated tests

❘  1029

Splitting your tests into categories makes them easier to identify and augment. It might also make it easier to realize which aspects of the code are well tested and which could use a few more unit tests.

WARNING  It’s easy to write a massive number of simple tests, but don’t forget about the more complicated cases!

Create Sample Data and Results The most common trap to fall into when writing unit tests is to match the test to the behavior of the code, instead of using the test to validate the code. If you write a unit test that performs a database select for a piece of data that is definitely in the database, and the test fails, is it a problem with the code or a problem with the test? It’s often easier to assume that the code is right and to modify the test to match. This approach is usually wrong. To avoid this pitfall, you should understand the inputs to the test and the expected output before you try it. This is sometimes easier said than done. For example, say you wrote some code to encrypt an arbitrary block of text using a particular key. A reasonable unit test would take a fixed string of text and pass it in to the encryption module. Then, it would examine the result to see if it was correctly encrypted. When you go to write such a test, it is tempting to try the behavior with the encryption module first and see the result. If it looks reasonable, you might write a test to look for that value. Doing so really doesn’t prove anything, however! You haven’t actually tested the code; you’ve just written a test that guarantees it will continue to return that same value. Oftentimes, writing the test requires some real work; you would need to encrypt the text independently of your encryption module to get an accurate result.

WARNING  Decide on the correct output for your test before you ever run the

test.

Write the Tests The exact code behind a test varies, depending on what type of test framework you have in place. One framework, the Microsoft Visual C++ Testing Framework, is discussed later in this chapter. Independent of the actual implementation, however, the following guidelines will help ensure effective tests: ➤➤

Make sure that you’re testing only one thing in each test. That way, if a test fails, it will point to a specific piece of functionality.

1030 

❘  CHAPTER 30   Becoming Adept at Testing

➤➤

Be specific inside the test. Did the test fail because an exception was thrown or because the wrong value was returned?

➤➤

Use logging extensively inside of test code. If the test fails someday, you will have some insight into what happened.

➤➤

Avoid tests that depend on earlier tests or are otherwise interrelated. Tests should be as atomic and isolated as possible.

➤➤

If the test requires the use of other subsystems, consider writing stubs or mocks to simulate those subsystems. A stub or mock implements the same interface as the subsystem it simulates. They can then be used in place of any concrete subsystem implementation. For example, if a unit test requires a database but that database is not the subsystem being tested by that unit test, then a stub or mock can implement the database interface and simulate a real database. This way, running the unit test does not require a connection to a real database, and errors in the real database implementation won’t have any impact on this specific unit test.

➤➤

Ask your code reviewers to look at your unit tests as well. When you do a code review, tell the other engineer where you think additional tests could be added.

As you will see later in this chapter, unit tests are usually small and simple pieces of code. In most cases, writing a single unit test will take only a few minutes, making them one of the most productive uses of your time.

Run the Tests When you’re done writing a test, you should run it right away before the anticipation of the results becomes too much to bear. The joy of a screen full of passing unit tests shouldn’t be minimized. For most programmers, this is the easiest way to see quantitative data that declares your code useful and correct. Even if you adopt the methodology of writing tests before writing code, you should still run the tests immediately after they are written. This way, you can prove to yourself that the tests fail initially. Once the code is in place, you have tangible data that shows that it accomplished what it was supposed to accomplish. It’s unlikely that every test you write will have the expected result the first time. In theory, if you are writing tests before writing code, all of your tests should fail. If one passes, either the code magically appeared or there is a problem with the test. If the coding is done and tests still fail (some would say that if tests fail, the coding is actually not done), there are two possibilities: the code could be wrong, or the tests could be wrong. Running unit tests must be automated. This can be done in several ways. One option is to have a dedicated system that automatically runs all unit tests after every continuous integration build, or at least once a night. Such a system must send out e-mails to notify developers when unit tests are failing. Another option is to set up your local development environment so that unit tests are executed every time you compile your code. For this, unit tests should be kept small and very efficient. If you do have longer-running unit tests, put these separate, and let these be tested by a dedicated test system.

Unit Testing 

❘  1031

Unit Testing in Action Now that you’ve read about unit testing in theory, it’s time to actually write some tests. The following example draws on the object pool implementation from Chapter 29. As a brief recap, the object pool is a class that can be used to avoid allocating an excessive number of objects. By keeping track of already-allocated objects, the pool acts as a broker between code that needs a certain type of object and such objects that already have been allocated. The public interface for the ObjectPool class template is as follows; consult Chapter 29, “Writing Efficient C++,” for all the details: export template class ObjectPool { public: ObjectPool() = default; explicit ObjectPool(const Allocator& allocator); virtual ~ObjectPool(); // Allow move construction and move assignment. ObjectPool(ObjectPool&& src) noexcept = default; ObjectPool& operator=(ObjectPool&& rhs) noexcept = default; // Prevent copy construction and copy assignment. ObjectPool(const ObjectPool& src) = delete; ObjectPool& operator=(const ObjectPool& rhs) = delete; // Reserves and returns an object from the pool. Arguments can be // provided which are perfectly forwarded to a constructor of T. template std::shared_ptr acquireObject(Args... args); };

Introducing the Microsoft Visual C++ Testing Framework Microsoft Visual C++ comes with a built-in testing framework. The advantage of using a unit testing framework is that it allows the developer to focus on writing tests instead of dealing with setting up tests, building logic around tests, and gathering results. The following discussion is written for Visual C++ 2019.

NOTE  If you are not using Visual C++, there are a number of open-source unit testing frameworks available. Google Test (github.com/google/googletest) is one such framework for C++, and the Boost Test Library (www.boost.org/doc/ libs/1_73_0/libs/test/) is another one. They both include a number of helpful utilities for test developers and options to control the automatic output of results.

1032 

❘  CHAPTER 30   Becoming Adept at Testing

To get started with the Visual C++ Testing Framework, you have to create a test project. The following steps explain how to test the ObjectPool class template:

1. 2. 3.

Start Visual C++, create a new project, select Native Unit Test Project, and click Next. Give the project a name and click Create. The wizard creates a new test project, which includes a file called .cpp. Select this file in the Solution Explorer and delete it, because you will add your own files. If the Solution Explorer docking window is not visible, go to View ➪ Solution Explorer.

4.

Right-click your project in the Solution Explorer and click Properties. Go to Configuration Properties ➪ C/C++ ➪ Precompiled Headers, set the Precompiled Header option to Not Using Precompiled Headers, and click OK. Additionally, select the pch.cpp and pch.h files in the Solution Explorer and delete them. Using precompiled headers is a feature of Visual C++ to improve build times but is not used for this test project.

5.

Add empty files called ObjectPoolTest.h and ObjectPoolTest.cpp to the test project.

Now you are ready to start adding unit tests to the code. The most common way is to divide your unit tests into logical groups of tests, called test classes. You will now create a test class called ObjectPoolTest. The basic code in ObjectPoolTest.h for getting started is as follows: #pragma once #include TEST_CLASS(ObjectPoolTest) { public: };

This code defines a test class called ObjectPoolTest, but the syntax is a bit different compared to standard C++. This is so that the framework can automatically discover all the tests. If you need to perform any tasks that need to happen prior to running the tests defined in a test class or to perform any cleanup after the tests have been executed, then you can implement an initialize and a cleanup method. Here is an example: TEST_CLASS(ObjectPoolTest) { public: TEST_CLASS_INITIALIZE(setUp); TEST_CLASS_CLEANUP(tearDown); };

Because the tests for the ObjectPool class template are relatively simple and isolated, empty definitions will suffice for setUp() and tearDown(), or you can simply remove them altogether. If you do need them, the beginning stage of the ObjectPoolTest.cpp source file is as follows: #include "ObjectPoolTest.h" void ObjectPoolTest::setUp() { } void ObjectPoolTest::tearDown() { }

Unit Testing 

❘  1033

That’s all the initial code you need to start developing unit tests.

NOTE  In real-world scenarios, you usually divide the testing code and the code you want to test into separate projects. In the interest of keeping this example succinct, I have not done this here.

Writing the First Test Because this may be your first exposure to the Visual C++ Testing Framework, or to unit tests at large, the first test will be a simple one. It tests whether 0 < 1. An individual unit test is just a method of a test class. To create a simple test, add its declaration to the ObjectPoolTest.h file: TEST_CLASS(ObjectPoolTest) { public: TEST_CLASS_INITIALIZE(setUp); TEST_CLASS_CLEANUP(tearDown); TEST_METHOD(testSimple);

// Your first test!

};

The implementation of the testSimple test uses Assert::IsTrue(), defined in the Microsoft:: VisualStudio::CppUnitTestFramework namespace, to perform the actual test. Assert::IsTrue() validates that a given expression returns true. If the expression returns false, the test fails. Assert provides many more helper functions, such as AreEqual(), IsNull(), Fail(), ExpectException(), and so on. In the testSimple case, the test claims that 0 is less than 1. Here is the updated ObjectPoolTest.cpp file: #include "ObjectPoolTest.h" using namespace Microsoft::VisualStudio::CppUnitTestFramework; void ObjectPoolTest::setUp() { } void ObjectPoolTest::tearDown() { } void ObjectPoolTest::testSimple() { Assert::IsTrue(0 < 1); }

That’s it. Of course, most of your unit tests will do something a bit more interesting than a simple assert. As you will see, the common pattern is to perform some sort of calculation and then assert that the result is the value you expect. With the Visual C++ Testing Framework, you don’t even need to worry about exceptions; the framework catches and reports them as necessary.

1034 

❘  CHAPTER 30   Becoming Adept at Testing

Building and Running Tests Build your solution by clicking Build ➪ Build Solution, and open the Test Explorer (Test ➪ Windows ➪ Test Explorer), shown in Figure 30‑4. After having built the solution, the Test Explorer automatically displays all discovered unit tests. In this case, it displays the testSimple unit test. You can run all tests by clicking the Run All Tests button in the upper-left corner of the window. When you do that, the Test Explorer shows whether the unit tests succeed or fail. In this case, the single unit test succeeds, as shown in Figure 30‑5. If you modify the code to assert that 1 < 0, the test fails, and the Test Explorer reports the failure, as shown in Figure 30‑6.

FIGURE 30-4 

FIGURE 30-5 

The lower part of the Test Explorer window displays useful information related to the selected unit test. In case of a failed unit test, it tells you exactly what failed. In this case, it says that an assertion failed. There is also a stack trace that was captured at the time the failure occurred. You can click the hyperlinks in that stack trace to jump directly to the offending line—very useful for debugging.

Negative Tests You can write negative tests, tests that do something that should fail. For example, you can write a negative test to test that a certain method throws an expected exception. The Visual C++ Testing Framework provides the Assert::ExpectException() function to handle expected exceptions. For example, the following unit test uses ExpectException() to execute a lambda expression that throws an std::invalid_argument exception. The template type parameter for ExpectException() specifies the type of exception to expect.

Unit Testing 

❘  1035

void ObjectPoolTest::testException() { Assert::ExpectException( []{ throw std::invalid_argument { "Error" }; }, L"Unknown exception caught."); }

FIGURE 30-6 

Adding the Real Tests Now that the framework is all set up and a simple test is working, it’s time to turn your attention to the ObjectPool class template and write some code that actually tests it. All of the following tests will be added to ObjectPoolTest.h and ObjectPoolTest.cpp, just like the earlier initial tests. First, copy the ObjectPool.cppm module interface file next to the ObjectPoolTest.h file you created, and then add it to the project. Before you can write the tests, you’ll need a helper class to use with the ObjectPool. The ObjectPool creates objects of a certain type and hands them out to the caller as requested. Some of the tests will need to check if a retrieved object is the same as a previously retrieved object. One way to do this is to create a pool of serial objects—objects that have a monotonically increasing serial number. The following code shows the Serial.cppm module interface file defining such a class: module; #include

// For size_t

export module serial; export class Serial { public: // A new object gets a next serial number. Serial() : m_serialNumber { ms_nextSerial++ } { }

continues

1036 

❘  CHAPTER 30   Becoming Adept at Testing

(continued) size_t getSerialNumber() const { return m_serialNumber; } private: static inline size_t ms_nextSerial { 0 }; // The first serial number is 0. size_t m_serialNumber; };

Now, on to the tests! As an initial sanity check, you might want a test that creates an object pool. If any exceptions are thrown during creation, the Visual C++ Testing Framework will report an error. The code is written according to the AAA principle: Arrange, Act, Assert; the test first sets up everything for the test to run, then does some work, and finally asserts the expected result. This is also often called the if-when-then principle. I recommend adding comments to your unit test that actually start with IF, WHEN, and THEN so the three phases of a test clearly stand out. void ObjectPoolTest::testCreation() { // IF nothing // WHEN creating an ObjectPool ObjectPool myPool; // THEN no exception is thrown }

Don’t forget to add a TEST_METHOD(testCreation); statement to the header file. This holds for all subsequent tests as well. You also need to add an import declaration for the object_pool and serial modules to the ObjectPoolTest.cpp source file. import object_pool; import serial;

A second test, testAcquire(), tests a specific piece of public functionality: the ability of the ObjectPool to give out an object. In this case, there is not much to assert. To prove the validity of the resulting Serial reference, the test asserts that its serial number is greater than or equal to zero. void ObjectPoolTest::testAcquire() { // IF an ObjectPool has been created for Serial objects ObjectPool myPool; // WHEN acquiring an object auto serial { myPool.acquireObject() }; // THEN we get a valid Serial object Assert::IsTrue(serial->getSerialNumber() >= 0); }

The next test is a bit more interesting. The ObjectPool should not give out the same Serial object twice. This test checks the exclusivity property of the ObjectPool by retrieving a number of objects from the pool. The retrieved objects are stored in a vector to make sure they aren’t automatically released back to the pool at the end of each for loop iteration. If the pool is properly dishing out unique objects, none of their serial numbers should match. This implementation uses the vector and set containers from the Standard Library; see Chapter 18, “Standard Library Containers.” The code requires , , and .

Unit Testing 

❘  1037

void ObjectPoolTest::testExclusivity() { // IF an ObjectPool has been created for Serial objects ObjectPool myPool; // WHEN acquiring several objects from the pool const size_t numberOfObjectsToRetrieve { 10 }; vector retrievedSerials; set seenSerialNumbers; for (size_t i { 0 }; i < numberOfObjectsToRetrieve; i++) { auto nextSerial { myPool.acquireObject() }; // Add the retrieved Serial to the vector to keep it 'alive', // and add the serial number to the set. retrievedSerials.push_back(nextSerial); seenSerialNumbers.insert(nextSerial->getSerialNumber()); } // THEN all retrieved serial numbers are different. Assert::AreEqual(numberOfObjectsToRetrieve, seenSerialNumbers.size()); }

The final test (for now) checks the release functionality. Once an object is released, the ObjectPool can give it out again. The pool shouldn’t allocate additional objects until it has recycled all released objects. The test first acquires 10 Serial objects from the pool, stores them in a vector to keep them alive, and records the raw pointer of each acquired Serial. Once all 10 objects have been retrieved, they are released back to the pool. The second phase of the test again retrieves 10 objects from the pool and stores them in a vector to keep them alive. All these retrieved objects must have a raw pointer that has already been seen during the first phase of the test. This validates that objects are properly reused by the pool. The implementation uses the std::sort() algorithm that requires . void ObjectPoolTest::testRelease() { // IF an ObjectPool has been created for Serial objects ObjectPool myPool; // AND we acquired and released 10 objects from the pool, while // remembering their raw pointers const size_t numberOfObjectsToRetrieve { 10 }; // A vector to remember all raw pointers that have been handed out by the pool. vector retrievedSerialPointers; vector retrievedSerials; for (size_t i { 0 }; i < numberOfObjectsToRetrieve; i++) { auto object { myPool.acquireObject() }; retrievedSerialPointers.push_back(object.get()); // Add the retrieved Serial to the vector to keep it 'alive'. retrievedSerials.push_back(object); } // Release all objects back to the pool. retrievedSerials.clear(); // The above loop has created 10 Serial objects, with 10 different // addresses, and released all 10 Serial objects back to the pool.

continues

1038 

❘  CHAPTER 30   Becoming Adept at Testing

(continued) // WHEN again retrieving 10 objects from the pool, and // remembering their raw pointers. vector newlyRetrievedSerialPointers; for (size_t i { 0 }; i < numberOfObjectsToRetrieve; i++) { auto object { myPool.acquireObject() }; newlyRetrievedSerialPointers.push_back(object.get()); // Add the retrieved Serial to the vector to keep it 'alive'. retrievedSerials.push_back(object); } // Release all objects back to the pool. retrievedSerials.clear(); // THEN all addresses of the 10 newly acquired objects must have been // seen already during the first loop of acquiring 10 objects. // This makes sure objects are properly re-used by the pool. sort(begin(retrievedSerialPointers), end(retrievedSerialPointers)); sort(begin(newlyRetrievedSerialPointers), end(newlyRetrievedSerialPointers)); Assert::IsTrue(retrievedSerialPointers == newlyRetrievedSerialPointers); }

If you add all these tests and run them, the Test Explorer should look like Figure 30‑7. Of course, if one or more tests fail, you are presented with the quintessential issue in unit testing: is it the test or the code that is broken?

Debugging Tests The Visual C++ Testing Framework makes it easy to debug unit tests that are failing. The Test Explorer shows a stack trace captured at the time a unit test failed, containing hyperlinks pointing directly to offending lines. However, sometimes it is useful to run a unit test directly in the debugger so that you can inspect variables at run time, step through the code line by line, and so on. To do this, you put a breakpoint on some line of code in your unit test. Then, you right-click the unit test in the Test Explorer and click Debug. The testing framework starts running the selected tests in the debugger and breaks at your breakpoint. From then on, you can step through the code however you want.

Basking in the Glorious Light of Unit Test Results

FIGURE 30-7 

The tests in the previous section should have given you a good idea of how to start writing professional-quality tests for real code. It’s just the tip of the iceberg, though. The previous examples should help you think of additional tests that you could write for the ObjectPool class template. For example, you could add a capacity() method to ObjectPool that returns the sum of the number of objects that have been handed out and the number of objects that are still available without allocating a new chunk of memory. This is similar to the capacity() method of vector that returns

Higher-Level Testing 

❘  1039

the total number of elements that can be stored in a vector without reallocation. Once you have such a method, you can include a test that verifies that the pool always grows by double the number of elements compared to the previous time the pool grew. There is no end to the number of unit tests you could write for a given piece of code, and that’s the best thing about unit tests. If you find yourself wondering how your code might react to a certain situation, that’s a unit test. If a particular aspect of your subsystem seems to be presenting problems, increase unit test coverage of that particular area. Even if you simply want to put yourself in the client’s shoes to see what it’s like to work with your class, writing unit tests is a great way to get a different perspective.

FUZZ TESTING Fuzz testing, also known as fuzzing, involves a fuzzer that automatically generates random input data for a program or component to try to find unhandled edge cases. Typically, a recipe is provided that specifies how input data needs to be structured so it can be used as input for the program. If clearly wrongly structured input is provided to a program, its input data parser will likely immediately reject it. A fuzzer’s job then is to try to generate input data that is not obviously wrongly structured, so it won’t be rejected immediately by the program, but that could trigger some faulty logic further along during the execution of the program. Since a fuzzer generates random input data, it requires a lot of resources to cover the entire input space. An option is to run such fuzz testing scenarios in a cloud. There are several libraries available for implementing fuzz testing, for example libFuzzer (llvm.org/ docs/LibFuzzer.html) and honggfuzz (github.com/google/honggfuzz).

HIGHER-LEVEL TESTING While unit tests are the best first line of defense against bugs, they are only part of the larger testing process. Higher-level tests focus on how pieces of the product work together, as opposed to the relatively narrow focus of unit tests. In a way, higher-level tests are more challenging to write because it’s less clear what tests need to be written. Still, you cannot really claim that the program works until you have tested how its pieces work together.

Integration Tests An integration test covers areas where components meet. Unlike a unit test, which generally acts on the level of a single class, an integration test usually involves two or more classes. Integration tests excel at testing interactions between two components, often written by two different programmers. In fact, the process of writing an integration test often reveals important incompatibilities in designs.

Sample Integration Tests Because there are no hard-and-fast rules to determine what integration tests you should write, some examples might help you get a sense of when integration tests are useful. The following scenarios depict cases where an integration test is appropriate, but they do not cover every possible case. Just as with unit tests, over time you will refine your intuition for useful integration tests.

1040 

❘  CHAPTER 30   Becoming Adept at Testing

A JSON-Based File Serializer Suppose that your project includes a persistence layer that is used to save certain types of objects to disk and to read them back in. The hip way to serialize data is to use the JSON format, so a logical breakdown of components might include a JSON conversion layer sitting on top of a custom file API. Both of these components can be thoroughly unit tested. The JSON layer can have unit tests that ensure that different types of objects are correctly converted to JSON and populated from JSON. The file API can have tests that read, write, update, and delete files on disk. When these modules start to work together, integration tests are appropriate. At the least, you should have an integration test that saves an object to disk through the JSON layer and then reads it back in and does a comparison to the original. Because the test covers both modules, it is a basic integration test.

Readers and Writers to a Shared Resource Imagine a program that contains a data structure shared by different components. For example, a stock-trading program can have a queue of buy-and-sell requests. Components related to receiving stock transaction requests can add orders to the queue, and components related to performing stock trades can take data off the queue. You can unit test the heck out of the queue class, but until it is tested with the actual components that will be using it, you really don’t know if any of your assumptions are wrong. A good integration test uses the stock request components and the stock trade components as clients of the queue class. You can write some sample orders and make sure that they successfully enter and exit the queue through the client components.

Wrapper Around a Third-Party Library Integration tests do not always need to occur at integration points in your own code. Many times, integration tests are written to test the interaction between your code and a third-party library. For example, you may be using a database connection library to talk to a relational database system. Perhaps you built an object-oriented wrapper around the library that adds support for connection caching or provides a friendlier interface. This is an important integration point to test because, even though the wrapper probably provides a more useful interface to the database, it introduces possible misuse of the original library. In other words, writing a wrapper is a good thing, but writing a wrapper that introduces bugs is going to be a disaster.

Methods of Integration Testing When it comes to actually writing integration tests, there is often a fine line between integration and unit tests. If a unit test is modified so that it touches another component, is it suddenly an integration test? In a way, the answer is moot because a good test is a good test, regardless of the type of test. I recommend you use the concepts of integration and unit testing as two approaches to testing, but avoid getting caught up in labeling the category of every single test.

Higher-Level Testing 

❘  1041

In terms of implementation, integration tests are often written by using a unit testing framework, further blurring their distinction. As it turns out, unit testing frameworks provide an easy way to write a yes/no test and produce useful results. Whether the test is looking at a single unit of functionality or the intersection of two components hardly makes a difference from the framework’s point of view. However, for performance and organizational reasons, you may want to attempt to separate unit tests from integration tests. For example, your group may decide that everybody must run integration tests before checking in new code, but be a bit laxer on running unrelated unit tests. Separating the two types of tests also increases the value of results. If a test failure occurs within the JSON class tests, it will be clear that it’s a bug in that class, not in the interaction between that class and the file API.

System Tests System tests operate at an even higher level than integration tests. These tests examine the program as a whole. System tests often make use of a virtual user that simulates a human being working with the program. Of course, the virtual user must be programmed with a script of actions to perform. Other system tests rely on scripts or a fixed set of inputs and expected outputs. Much like unit and integration tests, an individual system test performs a specific test and expects a specific result. It is not uncommon to use system tests to make sure that different features work in combination with one another. In theory, a fully system-tested program would contain a test for every permutation of every feature. This approach quickly grows unwieldy, but you should still make an effort to test many features in combination. For example, a graphics program could have a system test that imports an image, rotates it, performs a blur filter, converts it to black and white, and then saves it. The test would compare the saved image to a file that contains the expected result. Unfortunately, few specific rules can be stated about system tests because they are highly dependent on the actual application. For applications that process files with no user interaction, system tests can be written much like unit and integration tests. For graphical programs, a virtual user approach may be best. For server applications, you might need to build stub clients that simulate network traffic. The important part is that you are actually testing real use of the program, not just a piece of it.

Regression Tests Regression testing is more of a testing concept than a specific type of test. The idea is that once a feature works, developers tend to put it aside and assume that it will continue to work. Unfortunately, new features and other code changes often conspire to break previously working functionality. Regression tests are often put in place as a sanity check for features that are, more or less, complete and working. If the regression test is well written, it will cease to pass when a change is introduced that breaks the feature. If your company has an army of quality-assurance testers, regression testing may take the form of manual testing. The tester acts as a user would and goes through a series of steps, gradually testing every feature that worked in the previous release. This approach is thorough and accurate if carefully performed, but is not particularly scalable.

1042 

❘  CHAPTER 30   Becoming Adept at Testing

At the other extreme, you could build a completely automated system that performs each function as a virtual user. This would be a scripting challenge, though several commercial and noncommercial packages exist to ease the scripting of various types of applications. A middle ground is known as smoke testing. Some tests will only test a subset of the most important features that should work. The idea is that if something is broken, it should show up right away. If smoke tests pass, they could be followed by more rigorous manual or automated testing. The term smoke testing was introduced a long time ago, in electronics. After a circuit was built, with different components like vacuum tubes, resistors, and so on, the question was, “Is it assembled correctly?” A solution was to “plug it in, turn it on, and see if smoke comes out.” If smoke came out, the design might be wrong, or the assembly might be wrong. By seeing what part went up in smoke, the error could be determined. Some bugs are like nightmares: they are both terrifying and recurring. Recurring bugs are frustrating and a poor use of engineering resources. Even if, for some reason, you decide not to write a suite of regression tests, you should still write regression tests for bugs that you fix. By writing a test for a bug fix, you both prove that the bug is fixed and set up an alert that is triggered if the bug ever comes back (for example, if your change is rolled back or otherwise undone, or if two branches are not merged correctly into the main development branch). When a regression test of a previously fixed bug fails, it should be easy to fix because the regression test can refer to the original bug number and describe how it was fixed the first time.

TIPS FOR SUCCESSFUL TESTING As a software engineer, your role in testing may range anywhere from basic unit testing responsibility to complete management of an automated test system. Because testing roles and styles vary so much, here are several tips from my experience that may help you in different testing situations: ➤➤

Spend some time designing your automated test system. A system that runs constantly throughout the day will detect failures quickly. A system that sends e-mails to engineers automatically, or sits in the middle of the room loudly playing show tunes when a failure occurs, will result in increased visibility of problems.

➤➤

Don’t forget about stress testing. Even if a full suite of unit tests passes for your database access class, it could still fall down when used by several dozen threads simultaneously. You should test your product under the most extreme conditions it could face in the real world.

➤➤

Test on a variety of platforms or a platform that closely mirrors the customer’s system. One method of testing on multiple operating systems is to use a virtual machine environment that allows you to run several different operating systems on the same physical machine.

➤➤

Some tests can be written to intentionally inject faults in a system. For example, you could write a test that deletes a file while it is being read, or that simulates a network outage during a network operation.

Exercises 

❘  1043

➤➤

Bugs and tests are closely related. Bug fixes should be proven by writing regression tests. A comment with a test could refer to the original bug number.

➤➤

Don’t remove tests that are failing. When a co-worker is slaving over a bug and finds out you removed tests, he will come looking for you.

The most important tip I can give you is to remember that testing is part of software development. If you agree with that and accept it before you start coding, it won’t be quite as unexpected when the feature is finished, but there is still more work to do to prove that it works.

SUMMARY This chapter covered the basic information that all professional programmers should know about testing. Unit testing in particular is the easiest and most effective way to increase the quality of your own code. Higher-level tests provide coverage of use cases, synchronicity between modules, and protection against regressions. No matter what your role is with regard to testing, you should now be able to confidently design, create, and review tests at various levels. Now that you know how to find bugs, it’s time to learn how to fix them. To that end, Chapter 31, “Conquering Debugging,” covers techniques and strategies for effective debugging.

EXERCISES By solving the following exercises, you can practice the material discussed in this chapter. Solutions to all exercises are available with the code download on the book’s website at www.wiley.com/go/ proc++5e. However, if you are stuck on an exercise, first reread parts of this chapter to try to find an answer yourself before looking at the solution from the website. Exercise 30-1: What are the three types of testing? Exercise 30-2: Make a list of unit tests that you can think of for the following piece of code: export class Foo { public: // Constructs a Foo. Throws invalid_argument if a >= b. Foo(int a, int b) : m_a { a }, m_b { b } { if (a >= b) { throw std::invalid_argument { "a should be less than b." }; } } int getA() const { return m_a; } int getB() const { return m_b; } private: int m_a, m_b; };

1044 

❘  CHAPTER 30   Becoming Adept at Testing

Exercise 30-3: If you are using Visual C++, implement the unit tests that you’ve listed in Exercise 30-2 using the Visual C++ Testing Framework. Exercise 30-4: Suppose you have written a function to calculate the factorial of a number. The factorial of a number n, written as n!, is the product of all numbers 1 to n. For example, 3!=1×2×3. You decided to follow the advice given in this chapter and to write unit tests for your code. You run the code to calculate 5! and then write a unit tests that verifies that the code produces the calculated number when asked to calculate the factorial of 5. What do you think of such a unit test?

31

Conquering Debugging WHAT’S IN THIS CHAPTER? ➤➤

The fundamental law of debugging, and bug taxonomies

➤➤

Tips for avoiding bugs

➤➤

How to plan for bugs

➤➤

The different kinds of memory errors

➤➤

How to use a debugger to pinpoint code causing a bug

WILEY.COM DOWNLOADS FOR THIS CHAPTER

Please note that all the code examples for this chapter are available as part of this chapter’s code download on the book’s website at www.wiley.com/go/proc++5e on the Download Code tab. Your code will contain bugs. Every professional programmer would like to write bug-free code, but the reality is that few software engineers succeed in this endeavor. As computer users know, bugs are endemic in computer software. The software that you write is probably no exception. Therefore, unless you plan to bribe your co-workers into fixing all your bugs, you cannot be a professional C++ programmer without knowing how to debug C++ code. One factor that often distinguishes experienced programmers from novices is their debugging skills. Despite the obvious importance of debugging, it is rarely given enough attention in courses and books. Debugging seems to be the type of skill that everyone wants you to know, but no one knows how to teach. This chapter attempts to provide concrete debugging guidelines and techniques. This chapter starts with the fundamental law of debugging and bug taxonomies, followed by tips for avoiding bugs. Techniques for planning for bugs include error logging, debug traces, assertions, and crash dumps. Specific tips are given for debugging the problems that arise, including techniques for reproducing bugs, debugging reproducible bugs, debugging Professional C++, Fifth Edition. Marc Gregoire. © 2021 John Wiley & Sons, Inc. Published 2021 by John Wiley & Sons, Inc.

1046 

❘  CHAPTER 31  Conquering Debugging

nonreproducible bugs, debugging memory errors, and debugging multithreaded programs. The chapter concludes with a step-by-step debugging example.

THE FUNDAMENTAL LAW OF DEBUGGING The first rule of debugging is to be honest with yourself and admit that your code will contain bugs! This realistic assessment enables you to put your best effort into preventing bugs from crawling into your code in the first place, while you simultaneously include the necessary features to make debugging as easy as possible.

WARNING  The fundamental law of debugging states that you should avoid bugs when you’re coding, but plan for bugs in your code.

BUG TAXONOMIES A bug in a computer program is incorrect run-time behavior. This undesirable behavior includes both catastrophic and noncatastrophic bugs. Examples of catastrophic bugs are program death, data corruption, operating system failures, or some other horrific outcome. A catastrophic bug can also manifest itself external to the software or computer system running the software; for example, medical software might contain a catastrophic bug causing a massive radiation overdose to a patient. Noncatastrophic bugs are bugs that cause the program to behave incorrectly in more subtle ways; for example, a web browser might return the wrong web page, or a spreadsheet application might calculate the standard deviation of a column incorrectly. These are also called logical bugs. There are also so-called cosmetic bugs, where something is visually not correct, but otherwise works correctly. For example, a button in a user interface is kept enabled when it shouldn’t be, but clicking it does nothing. All computations are perfectly correct, the program does not crash, but it doesn’t look as “nice” as it should. The underlying cause, or root cause, of a bug is the mistake in the program that causes this incorrect behavior. The process of debugging a program includes both determining the root cause of the bug and fixing the code so that the bug will not occur again.

AVOIDING BUGS It’s impossible to write completely bug-free code, so debugging skills are important. However, a few tips can help you to minimize the number of bugs: ➤➤

Read this book from cover to cover: Learn the C++ language intimately, especially pointers and memory management. Then, recommend this book to your friends and co-workers so they avoid bugs too.

➤➤

Design before you code: Designing while you code tends to lead to convoluted designs that are harder to understand and are more error-prone. It also makes you more likely to omit possible edge cases and error conditions.

Planning for Bugs 

❘  1047

➤➤

Do code reviews: In a professional environment, every single line of code should be peerreviewed. Sometimes it takes a fresh perspective to notice problems.

➤➤

Test, test, and test again: Thoroughly test your code, and have others test your code! They are more likely to find problems you haven’t thought of.

➤➤

Write automated unit tests: Unit tests are designed to test isolated functionality. You should write unit tests for all implemented features. Run these unit tests automatically as part of your continuous integration setup, or automatically after each local compilation. Chapter 30, “Becoming Adept at Testing,” discusses unit testing in detail.

➤➤

Expect error conditions, and handle them appropriately: In particular, plan for and handle errors when working with files and network connections. They will occur. See chapters 13, “Demystifying C++ I/O,” and 14, “Handling Errors.”

➤➤

Use smart pointers to avoid memory leaks: Smart pointers automatically free resources when they are not needed anymore.

➤➤

Don’t ignore compiler warnings: Configure your compiler to compile with a high warning level. Do not blindly ignore warnings. Ideally, you should enable an option in your compiler to treat warnings as errors. This forces you to address each warning immediately. With GCC you can pass ‑Werror to the compiler to treat all warnings as errors. In Visual C++, open the properties of your project, go to Configuration Properties ➪ C/C++ ➪ General, and enable the option Treat Warnings As Errors.

➤➤

Use static code analysis: A static code analyzer helps you to pinpoint problems in your code by analyzing your source code. Ideally, static code analysis is done in real time while typing code in your integrated development environment (IDE) to detect problems early. It can also be set up to run automatically by your build process. There are quite a few different analyzers available on the Internet, both free and commercial.

➤➤

Use good coding style: Strive for readability and clarity, use meaningful names, don’t use abbreviations, add code comments (not only interface comments), use the override keyword, and so on. This makes it easier for other people to understand your code.

PLANNING FOR BUGS Your programs should contain features that enable easier debugging when the inevitable bugs arise. This section describes these features and presents sample implementations, where appropriate, that you can incorporate into your own programs.

Error Logging Imagine this scenario: You have just released a new version of your flagship product, and one of the first users reports that the program “stopped working.” You attempt to pry more information from the user and eventually discover that the program died in the middle of an operation. The user can’t quite remember what he was doing or if there were any error messages. How will you debug this problem? Now imagine the same scenario, but in addition to the limited information from the user, you are also able to examine the error log on the user’s computer. In the log you see a message from your program

1048 

❘  CHAPTER 31  Conquering Debugging

that says, “Error: unable to open config.xml file.” Looking at the code near the spot where that error message was generated, you find a line in which you read from the file without checking whether the file was opened successfully. You’ve found the root cause of your bug! Error logging is the process of writing error messages to persistent storage so that they will be available following an application, or even machine, death. Despite the example scenario, you might still have doubts about this strategy. Won’t it be obvious by your program’s behavior if it encounters errors? Won’t the user notice if something goes wrong? As the preceding example shows, user reports are not always accurate or complete. In addition, many programs, such as the operating system kernel and long-running daemons like inetd (internet service daemon) or syslogd on Unix, are not interactive and run unattended on a machine. The only way these programs can communicate with users is through error logging. In many cases, a program might also want to automatically recover from certain errors and hide those errors from the user. Still, having logs of those errors available can be invaluable to improve the overall stability of the program. Thus, your program should log errors as it encounters them. That way, if a user reports a bug, you will be able to examine the log files on the machine to see if your program reported any errors prior to encountering the bug. Unfortunately, error logging is platform dependent: C++ does not contain a standard logging mechanism. Examples of platform-specific logging mechanisms include the syslog facility in Unix and the event reporting API in Windows. You should consult the documentation for your development platform. There are also some open-source implementations of cross-platform logging frameworks. Here are two examples: ➤➤

log4cpp at log4cpp.sourceforge.net

➤➤

Boost.Log at boost.org

Now that you’re convinced that logging is a great feature to add to your programs, you might be tempted to log messages every few lines in your code so that, in the event of any bug, you’ll be able to trace the code path that was executing. These types of log messages are appropriately called traces. However, you should not write these traces to log files for two reasons. First, writing to persistent storage is slow. Even on systems that write the logs asynchronously, logging that much information will slow down your program. Second, and most important, most of the information that you would put in your traces is not appropriate for the end user to see. It will just confuse the user, leading to unwarranted service calls. That said, tracing is an important debugging technique under the correct circumstances, as described in the next section. Here are some specific guidelines for the types of errors you should log: ➤➤

Unrecoverable errors, such as a system call failing unexpectedly.

➤➤

Errors for which an administrator can take action, such as low memory, an incorrectly formatted data file, an inability to write to disk, or a network connection being down.

➤➤

Unexpected errors such as a code path that you never expected to take or variables with unexpected values. Note that your code should “expect” users to enter invalid data and should handle it appropriately. An unexpected error represents a bug in your program.

➤➤

Potential security breaches, such as a network connection attempted from an unauthorized address, or too many network connections attempted (denial of service).

Planning for Bugs 

❘  1049

It is also useful to log warnings, or recoverable errors, which allows you to investigate if you can possibly avoid them. Most logging APIs allow you to specify a log level or error level, typically error, warning, and info. You can log non-error conditions under a log level that is less severe than “error.” For example, you might want to log significant state changes in your application, or startup and shutdown of the program. You also might consider giving your users a way to adjust the log level of your program at run time so that they can customize the amount of logging that occurs.

Debug Traces When debugging complicated problems, public error messages generally do not contain enough information. You often need a complete trace of the code path taken, or values of variables before the bug showed up. In addition to basic messages, it’s sometimes helpful to include the following information in debug traces: ➤➤

The thread ID, if it’s a multithreaded program

➤➤

The name of the function that generates the trace

➤➤

The source filename in which the code that generates the trace lives

You can add this tracing to your program through a special debug mode, or via a ring buffer. Both of these methods are explained in detail in the following sections. Note that in multithreaded programs you have to make your trace logging thread-safe. See Chapter 27, “Multithreaded Programming with C++,” for details on multithreaded programming.

WARNING  Trace files can be written in text format, but if you do, be careful with logging too much detail. You don’t want to leak intellectual property through your log files! An alternative is to write the files in a binary format that only you can read.

Debug Mode A first technique to add debug traces is to provide a debug mode for your program. In debug mode, the program writes trace output to standard error or to a file, and perhaps does extra checking during execution. There are several ways to add a debug mode to your program. Note that all these examples are writing traces in text format.

Start-Time Debug Mode Start-time debug mode allows your application to run with or without debug mode depending on a command-line argument. This strategy includes the debug code in the “release” binary and allows debug mode to be enabled at a customer site. However, it does require users to restart the program to run it in debug mode, which may prevent you from obtaining useful information about certain bugs. The following example is a simple program implementing a start-time debug mode. This program doesn’t do anything useful; it is only for demonstrating the technique.

1050 

❘  CHAPTER 31  Conquering Debugging

All logging functionality is wrapped in a Logger class. This class has two static data members: the name of the log file and a Boolean saying whether logging is enabled or disabled. The class has a static public log() variadic template method. Variadic templates are discussed in Chapter 26, “Advanced Templates.” Note that the log file is opened, flushed, and closed on each call to log(). This might lower performance a bit; however, it does guarantee correct logging, which is more important. class Logger { public: static void enableLogging(bool enable) { ms_loggingEnabled = enable; } static bool isLoggingEnabled() { return ms_loggingEnabled; } template static void log(const Args&... args) { if (!ms_loggingEnabled) { return; } ofstream logfile { ms_debugFilename, ios_base::app }; if (logfile.fail()) { cerr DoubleIt(1.2) }; std::cout