Software Engineering 1 /48
Topics 1. The Compilation Process and You 2. Polymorphism and Composition 3. Small Functions 4. Comments 2 /48
The Compilation Process and You 3 / 48
1. Intro - How do you turn code into a program? - When you hit build, a number of processes are invoked to do this job - Preprocessor - Compiler - Assembler - Linker - It is important to understand these steps 4 / 48
2. The Preprocessor - The preprocessor runs on your code before anything else - It prepares your code for compilation - It executes statements starting with the # symbol 5 / 48
2.a. #include - #include headerfile.h - This replaces the include statement with the contents of the specified file - Use mainly to include header files - You can include other things - Don t 6 / 48
2.b. #define - #define [macro] [replacement] - The define keyword replaces the first term with the second term - Example: #define MAX_THINGS 1024 - Do not use this to conveniently define constants (like above) - Use C++ instead to get compile-time checks 7 / 48
2.b. #define (function macros) - You can also use #define to define function macros - You may have seen this in CS33 - That was C - This is C++ - Do not make function macros - Use inline C++ functions instead 8 / 48
2.b. #define (plain macros) - #define [macro] - You can also just define a special word - Can be done in code - Usually passed as an argument to the compiler - Specify these in Qt s.pro file - This should be your main use of #define - This becomes useful with the next directive 9 / 48
2.c. #ifdef, #ifndef, #endif - #ifdef defined_macro // if defined compile this code // compiles this #endif - #ifndef defined_macro // if NOT defined compile this code // compile this #endif - Using the previous #define macro, this does exactly what you think it does 10 / 48
2.c. #ifdef, #ifndef, #endif - The most common use of #ifdef, #ifndef, #endif is in header guards like this #ifndef HEADER_NAME_H #define HEADER_NAME_H // All code in your header #endif - This prevents headers from being included more than once any time you compile 11 / 48
2. Preprocessor cont. - The preprocessor does text replacement - It is not executed when your program runs - Its directives are not live code, so don t use it for actual program logic - Its logic is carried out and all macros are evaluated before the compiler touches the code 12 / 48
3. The Compiler - The compiler takes each source file generated by the preprocessor and compiles it into assembly language - From there, the assembler compiles the assembly into object code - The assembler is an intermediate step between the compiler and the linker - It is purely mechanical and not important for us 13 / 48
3. The Compiler - The major relevant parts of the compiler (for us) are the compiler flags - These specify certain behavior - Optimization - Warning and error generation - And more! - Different compilers exist; SunLab uses gcc - We ll go over just a few of gcc s options 14 / 48
3. The Compiler - -Wall: All warnings should be reported - Turn this on! - In.pro file, add QMAKE_CXXFLAGS += -Wall - You will learn lots of little bits from seeing everything - -Werror: All warnings become errors - Hardcore version of above: you can t compile if you have ANY warnings - Turn this on if you want to force yourself to code proper C++ 15 / 48
3. The Compiler - -O or -O1: Turn on level 1 optimizations - -O2: Optimize more - -O3: Optimize morer - -O0 (That s O and zero): Turn off optimizations - Will be easiest to use with the debugger - Look up specs for details, but they allow the compiler to make your code run faster - Hurray! 16 / 48
4. The Assembler - The assembler takes assembly code as written by the compiler - Assembly is written as a list of commands - Move this here, add this to that - The assembler translates each command into a numerical opcode (this is machine executable) - C++ supports compiling straight to machine code, to assembly code, to static or shared libraries, to whatever, really! 17 / 48
5. The Linker - Header files usually say that certain things (classes, functions, etc.) will exist (i.e. they ve been declared) - Implementations are defined in source files - The linker will pull in external libraries and source files to make an executable - E.G. Windows.dll files,.a libraries, etc. 18 / 48
6. Linker Errors - Oh I think I ll include this header file inside this other header file - Prefer forward declaration until you must include - Important exception: Always include Standard Library headers. Forward declaring is undefined behavior - *gasp* - The std namespace is reserved 19 / 48
6. Linker Errors - What is a forward declaration? - Rather than typing #include Foo.h in your.h file - Type class Foo; instead and put the include in the.cpp file - This signifies that the class Foo will exist and be defined later - This is all you need to say - Including the header file imports code and dependencies - This says what you need to say, but says too much more 20 / 48
6. Linker Errors - When can you forward declare Class Foo in header files? - When a Foo object is an argument or return type of a function - Pointer and reference versions of the above - This includes smart pointers* - When a member variable is a pointer or reference to Foo *The smart pointer headers themselves must be included 21 / 48
6. Linker Errors - When must you include Foo.h inside another header file? - When extending Foo - When an object is a direct (non-reference, non-pointer) member of Foo - When you want something from the Standard Library - Rule of thumb: Forward declare unless your compiler yells at you 22 / 48
6.a. Link times - If your project has lots of code to link, the Linker has more symbols to resolve and more places to look for them - Forward declaring is one statement for one class or function - Including shoves everything else about those functions and classes into the mix as well 23 / 48
6.b. Circular Dependencies A circular dependency occurs when two header files include one another - Header A includes header B - Header B includes header A - Header A includes itself - an infinite regression - Common example: Two classes contain instances of one another - Class A has member Class B - Class B has member Class A - In order to fully define class B for inclusion in Class A (and determine storage), class A must be fully defined, which requires that class B be fully defined 24 / 48
6.b. Circular Dependencies - Solution: Use pointers instead of direct members; forward declare instead of #include Foo.h Bar.h class Bar; // Forward Declaration class Foo; // Forward Declaration class Foo { class Bar { private: private: Bar *m_bar; // Member pointer Foo *m_bar; // Member pointer }; }; 25 / 48
7. Cleaning - The compiler and linker are smart and try to reduce their workload - They won t rebuild things unless they need to - Sometimes you ll change an aspect of your build process - Add compilation flag - Add #define macro - Have a weird time replacing a file - Can end up with mixed-build state 26 / 48
7. Cleaning - Solution: Clean your project - Qt->Build->Clean All - Removes all generated files - The next build will be forced to recompile everything - When in doubt, clean it out! - If you re getting strange errors that don t reflect the code, the code just may need to be cleaned 27 / 48
Polymorphism & Composition Prefer Composition! 28 /48
Classes are types - A class defines a type - The type represents data that has various functions defined for it - int is a type. So is a glm::vec3. So are Mesh and Shader. - We usually name functions like dothing() and length() - + and - are also functions - They take two arguments and return a result - Operators are functions - You should be able to use a class without knowing how it works - You know how to use int. You don t care how int is implemented. 29 /48
Polymorphism: How to do it - Liskov Substitution Principle: Derived types must be completely substitutable for their base types. - What this means: subclasses must work as intended if they are substituted for their superclasses - How not to do it: - Class Rectangle implements a bunch of methods, including setheight and setwidth (and getters) - Class Square subclasses Rectangle, but can t modify height alone - This is bad! Squares can t be treated as rectangles! 30 /48
Inheritance - A base class defines an interface (signatures of all its methods) - The base class only implements functionality common to every possible subclass - Correspondingly, all subclasses must obey this interface - What does that mean? - Base class does not do things conveniently for some subset of possible subclasses - Subclasses can be used as the base class without requiring the user to know which particular subclass it is - New subclasses do not require updates to base class - New subclasses do not need to circumvent/reverse-engineer parts of baseclass 31 /48
Filter example: How not to use the base class - Filter example: You notice that 2D convolution is shared by some classes, so you stick it in the base class. - But some subclasses can use 2 1D convolutions, so you put that in the base class. - And so on and so on until the base class implements everything - It has become a bucket 32 /48
Composition - Prefer composition over inheritance - By factoring shared functionality out into separate classes rather than up into the base class, you allow for unbridled customization without repeating code - By factoring up, you ve ensured that future subclasses must obey certain implementation details (in C++ especially if you didn t make functions virtual!) - This does not mean you should make everything virtual. That adds unnecessary overhead cost to your classes. - Composition can be used alongside inheritance - The distinction is more a question of where do you get your stuff? 33 /48
Inheritance example - Class Car defines functions startup, accelerate, brake, turn, and shutdown - Class Cars will have lots of subtypes - Subarus, Ferraris, and even Priuses - But a Ferrari engine is very different from a Subaru engine - Protected methods for subclasses to modify the engine - This quickly becomes tedious - all the Car subclasses are always out of date - And what happens when you need to make a Prius... 34 /48
Inheritance example - What if you make a separate engine class for your car - Now, additions to the Engine class will not require that Car update its interface - The Engine class is robust - it does everything you could ever possibly want, even stuff we don t need right now - The Engine class functionality is in the Car class via composition - Despite how robust the Engine class is - it does not provide Ferrari functionality - can t predict all future use cases 35 /48
Inheritance example - What if Car has a private variable Engine m_engine - Methods startup, shutdown, accelerate, etc. - Now we want a RaceCar that needs a souped up RaceEngine - But we don t have access to the base class s m_engine -> we make m_raceengine - All the base class methods that use the built-in m_engine now need to be overridden to use the new m_raceengine - Conclusion: none of the inherited code can be used - And you need to know how to reverse engineer the base class s methods that were using the built-in Engine, which, if they re private, is impossible - Solution: Don t have an Engine in the base class. Implement startup by referring to the sub-class s instance variable of an Engine 36 /48
Inheritance example - The new Car class only has one method: drive() - Any interface for acceleration + braking will be too simple for some Cars, but not robust enough for others. - Users now have to figure out all their own Car functionality - Car class doesn t actually do anything. - If you make no restrictions, you ve made no abstraction - The goal in creating an interface or a library is precisely to figure out the right abstractions. - And be flexible to make changes to account for constantly evolving contract requirements 37 /48
Inheritance example - One day, you may observe your code and exclaim, Oh look, this code is repeated in subclass X and subclass Y! I ll factor it out into the base class! - That is not how repeated code works - That is not how inheritance works - Repeated code is code that does the same thing for the same user in more than one location - The base class is not a receptacle for all potentially shared pieces of particular functionality for particular subclasses. 38 /48
Interface example - The base class lays out an interface! - Particular subclass implementation details are factored out into other classes - Subclasses X and Y that use M include it by composition - Subclass Z is now not forced to use M but may instead include N instead - Implementation details common to all subclass may be factored up into the base class - But even then it may make sense to factor it out into another class that B includes by composition 39 /48
Shapes Example - If Shape lays out particular OpenGL implementation details, it restricts what future subclasses can do - This has nothing to do with the way you actually use Shapes - i.e. the contract - Example: Shape lays out uv-tessellation implementation - How to implement a Mesh class? - A fractal class? - Example: Shape requires only GL_TRIANGLES - What about an efficient GL_TRIANGLE_STRIP class? - What about indexed buffers? - Solution: Direct OpenGL functionality (like raw VBOs) should not go in Shape base class - Shape subclasses should include OpenGL objects/functions by composition 40 /48
Inheritance Example - You know how the base class is supposed to be used - By definition, you do not know how a subclass will be implemented - If the base class provides an implementation of a certain piece of functionality, ask yourself this question: - Can I dream up any subclass that fits into the base class s interface but would not be possible with the base class s implementation of the functionality? - If no, then include the functionality in the base class - If yes, then remove the functionality from the base class 41 /48
Small Functions Keyword: small 42 /48
Large Functions - Hard to read - Need to be refactored more often - Large size means more places for bugs to hide - Usually break the Single Responsibility Principle - If it can be broken up into two functions, why isn t it two functions? 43 /48
Small Functions - Do one thing and one thing only - Say exactly what they do in the function name - There are no unintended consequences when they change - Because they only have one task, they are easier to debug 44 /48
Comments Yes, sometimes no 45 /48
What can go wrong with comments - Comments can go out of date if they re not updated when their code is - Comments can lie *gasp* - Comments can add clutter - Solution: - Take the time to descriptively name your variables and functions. If things are named obviously, there may not be a need to comment with an explanation - Use small functions - Good variable names alone will not make your code clear! You will need comments sometimes, and that is okay. 46 /48
When to use comments - To add new information - To explain how or why your code is working - Instead of saying: Step 4: Do X - Say: Step 4: Do X because reason - To explain how third parties use the public API you provide - Call this function with arguments that look like this:. - Returns an integer within this range:. - This throws an error when your arguments don t look like and what happens is. 47 /48