Homework 2: Written Part
Before you do this part, decide whether you'll do it as a pair or individually. For the written portion of the assignment (only) you can choose.
Complete the written questions below. (They'll give you some additional practice with separate compilation.) Your answers are saved as you go, and you can come back and change them until the deadline.
Readings
Read one (or both!) of these contextual readings about the rich and intertwined history between textiles and computation.
- From Weaving to Computing [PDF], or
- Hilda Wove All Those Wires [PDF]
I'm with my partner and we're ready to code. Can we do the reading later?
Yes, you can skip this part for now and come back to it later, if that works better for you!
Separate Compilation
In this exercise, you will get a chance to consider how compiling C++ code works when your programs get very large. We'll look at three different ways of organizing the code for two programs:
appreciate- Asks for a name from “standard input” (
std::cin, usually the user keyboard) and then prints a well-known “2, 4, 6, 8, …” cheer for that name. countdown- Simulates a rocket launch.
Both programs use a std::vector<int> (an object that simulates an indexed array of integers that can grow or shrink as needed, similar to an ArrayList in Java or a list in Python) and both programs call a helper function called printVector.
If you haven't already, clone the code repository for this assignment. In the separate-compilation directory, we have provided three different ways (badidea1, badidea2, and goodidea) to organize the code for appreciate and countdown. You will want to consult that code as you answer the following questions.
Bad Idea 1
For our first organizational attempt, we kept the two programs completely separate. Everything appreciate needs is included in the appreciate.cpp file and everything countdown needs is included in the file countdown.cpp.
For appreciate, we run
clang++ -c -std=c++20 -pedantic -Wall -Wextra appreciate.cpp
clang++ -o appreciate appreciate.o
which causes the compiler (clang++) to translate the human-readable code in appreciate.cpp into object code (appreciate.o) and then into an executable machine-language program called appreciate.
We build countdown the same way:
clang++ -c -std=c++20 -pedantic -Wall -Wextra countdown.cpp
clang++ -o countdown countdown.o
Bad Idea 2
Having to copy-and-paste code between files by hand as we did in Bad Idea 1 should have set off alarm bells in your mind. Any time we find ourselves even thinking about copying and pasting code, we should look for a better way!
In Bad Idea 2, we put the code for printVector() into its own file, printvector.hpp. Then we use #include to instruct clang++ to automatically copy-and-paste the contents of printvector.hpp into countdown.cpp and appreciate.cpp at the point where the #include lines appear.
Our two programs can be compiled with the same commands we used in Bad Idea 1:
clang++ -c -std=c++20 -pedantic -Wall -Wextra appreciate.cpp
clang++ -o appreciate appreciate.o
for appreciate, and
clang++ -c -std=c++20 -pedantic -Wall -Wextra countdown.cpp
clang++ -o countdown countdown.o
for countdown.
Meh. We only have to edit one file now, but we still have to do just as much work to compile everything!
True. Let's try something else.
Good Idea
While Bad Idea 2 has some advantages over Bad Idea 1 (at least we aren't still copying and pasting code by hand!), it's still not taking full advantage of the modularity of our code. Once we've translated the code for printvector.hpp into machine instructions once, it's silly to compile it again if its source code hasn't changed.
Our third attempt splits our code into four different files:
printvector.hpp- Includes the declaration of the
printVector()function; a promise to the compiler that machine code for aprintVector()function will exist eventually. printvector.cpp- Gives the definition of
printVector(). countdown.cppandappreciate.cpp- Are no longer completely separate pieces of code but rely on the promise (expressed in the
printvector.hppfile they both now#include) that machine code forprintVector()will be defined elsewhere.
Now we need five commands to build our programs,
clang++ -c -std=c++20 -pedantic -Wall -Wextra printvector.cpp
creates printvector.o from the code in printvector.cpp. Now we can build appreciate with
clang++ -c -std=c++20 -pedantic -Wall -Wextra appreciate.cpp
clang++ -o appreciate appreciate.o printvector.o
The first line creates an object file from the code in appreciate.cpp (and printvector.hpp, which is #included in appreciate.cpp). It's the same command we used in our previous examples.
But the second line is different:
clang++ -o appreciate appreciate.o printvector.o
We've added printvector.o to our command, so clang++ links both appreciate.o and printvector.o to create a complete executable file called appreciate.
We do the same thing with countdown:
clang++ -c -std=c++20 -pedantic -Wall -Wextra countdown.cpp
clang++ -o countdown countdown.o printvector.o
In summary, appreciate.cpp and countdown.cpp are first translated to machine instructions (.o files) based on just the declaration (interface specification) of the printVector() function. Then, to form executables, appreciate.o and countdown.o are each linked with the definition of printVector(), whose machine instructions are in printvector.o.
Good Idea is the way that C++ programmers use separate compilation in practice:
- Put function definitions in source files (
.cpp). - Put function declarations in header files (
.hpp). - Only ever
#includeheader files, and only when the declarations are needed. - Only ever compile source files (
.cpp). - When changes are made, recompile only the necessary source files and then relink the executable(s).
The advantages of this method (both in reducing code duplication and having to recompile less code) are even more apparent in “real world” C++ programs of nontrivial size, where compilation might take many minutes or even hours.
Unfortunately, while we've made it possible to spend less time compiling code, an obvious disadvantage of this approach is that it becomes harder to figure out which commands you need to run to rebuild everything you need to get your final executable program when you've changed a particular source-code file.
So now we have even more things to keep track of? I think I'll just keep all my code in one file.
We've added some complexity, yes, but we're computer scientists—we love having opportunities to simplify complex situations!
(When logged in, completion status appears here.)