Skip to main content

Section 12.12 Case Study: Building a Multi-File Program

Now that we know how to separate code into multiple files, we are ready to look at implementing the designs we came up with in SectionΒ 12.7. Since we have already done the design for our functions, we will skip directly to writing code for the β€œTop Down” design.

Subsection 12.12.1 The project structure

We will be building a single project (set of files) with two programs. One program will be the program we want to write - one that reads in two dates and calculates the difference between them. The other program will be a test program that verifies functions work correctly.

Note 12.12.1.

Depending on the development environment you are using outside of this book, the practicalities of setting up two programs that build from overlapping files may vary. You may need to set up two separate β€œprojects” in your development environment or you may be able to set up one β€œproject” with multiple build targets.
The code will be split into multiple files, with the following structure:
  • DateFunctions.cxx: will contain the implementations of the date-related functions. We will be implementing it as a module called DateFunctions. We could instead build it as a .h/.cpp file pair - that implementation is available in the appendix to this chapter.
  • main.cpp: the main file for the β€œreal” program. It will have the main function. It will import DateFunctions to help do its work.
  • dateTests.cpp: the test program that verifies the functions from DateFunctions work correctly.
We will use incremental development to build our program in small steps, testing each part as we go. We will also use test driven developmentβ€”for each function that we implement, we will start by writing a test for it first and then using that test to develop the function. Only after the function is working, will we try to integrate it into the β€œreal” program.

Subsection 12.12.2 Implementing getMonth

We need to pick somewhere to start. It makes sense to pick a getMonth function first, as it is a simple function that does not depend on anything else.
We will start by writing tests for the getMonth function. This will help us define the expected behavior of the function before we implement it. The two important valid cases are if the month has 1 or 2 digits. We should have tests for both of those.
We should also consider that happens if there is an invalid value. Either a number like 13 or a non-numeric string. We will throw an exception in those cases as it is not clear from the context of that low level function what the right way to handle the issue is. (Recall that the core idea of exceptions is to allow us to detect an issue in low-level code and propagate it to higher levels that might better know how to handle it.)
Here are some test cases. We would expect the following input/output pairs for the function:
Input Output
"3/4/2023" 3
"12/31/1999" 12
"1/1/2001" 1
"15/1/2001" Throw exception
"1a5/1/2001" Throw exception
Written as unit tests in dateTests.cpp, this will look like the following:
Listing 12.12.1. dateTests.cpp - getMonth version
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include "doctest.h"

import DateFunctions;

TEST_CASE("daysBeforeMonth") {
    CHECK(daysBeforeMonth(1) == 0);
    CHECK(daysBeforeMonth(2) == 31);
    CHECK(daysBeforeMonth(4) == 90);
    CHECK(daysBeforeMonth(12) == 334);
}

TEST_CASE("daysBeforeMonth invalid inputs") {
    CHECK_THROWS(daysBeforeMonth(0));
    CHECK_THROWS(daysBeforeMonth(13));
    CHECK_THROWS(daysBeforeMonth(-1));
}

TEST_CASE("getMonth") {
    CHECK(getMonth("3/4/2023") == 3);
    CHECK(getMonth("12/31/1999") == 12);
    CHECK(getMonth("1/1/2001") == 1);
}

TEST_CASE("getMonth invalid inputs") {
    CHECK_THROWS(getMonth("13/1/2023"));
    CHECK_THROWS(getMonth("abc/1/2023"));
    CHECK_THROWS(getMonth("1a5/1/2023"));
}
With those tests in place, we are ready to implement the getMonth function in DateFunctions.cxx:
Listing 12.12.2. DateFunctions.cxx - getMonth implementation
module;

#include <string>
#include <stdexcept>

export module DateFunctions;

using namespace std;

export int getMonth(const string& date) {
    size_t slash = date.find('/');
    if (slash == string::npos) {
        throw logic_error("Date must contain '/'");
    }

    string monthPart = date.substr(0, slash);
    // stoi will throw exceptions for invalid input
    int monthNum = stoi(monthPart);

    if (monthNum < 1 || monthNum > 12) {
        throw logic_error("Month out of range");
    }

    // If we reach here, the month is valid
    return monthNum;
}
Now we can go back to compile the tests with our module. The following activecode is set up to compile both the test tile (shown), as well as our module file, using a recipe like:
g++ -std=c++20 -fmodules-ts dateTests.cpp DateFunctions.cxx -o test-program.exe
Listing 12.12.3.

Checkpoint 12.12.1.

Which test case fails?
  • The test for the invalid month 1a5
  • Correct. It turns out that stoi will turn 1a5 into 1, ignore the a5, and not throw an exception. To see what the function actually returns, we can add some print statements or use a debugger.
  • The test for the valid month 12
  • The test for the invalid month 13
  • The test for the invalid month abc
It looks like we have a bug to fix.

Subsection 12.12.3 Fixing getMonth

Before going on to other functions, we should fix getMonth. It appears we will need to scan the monthNum string to make sure that each character is a digit.
We can do this by using a loop to check each character in the string. Something like:
Listing 12.12.4.
for(char c: monthPart) {
    if(!isdigit(c)) {
        throw logic_error("Month must be a number");
    }
}

Activity 12.12.1.

Try adding the loop shown above (or similar code that checks if all the characters are digits) to this copy of DateFunctions.cxx. (Right after the monthPart variable is assigned is a logical place). The activecode is set to compile, but not link. It will tell you if you have a compile error, but not if your implementation is correct. (We will check that below.)

Checkpoint 12.12.2.

After you have added the recommended code to ActivityΒ 12.12.1, rerun the copy of the tests below and make sure the tests all pass.

Subsection 12.12.4 Using getMonth in the real program

Now that we have implemented and tested getMonth, we could use it in our main program. According to our design (ExampleΒ 12.7.1), we won’t actually call getMonth from main. It will be used by dateToDays, which is called by daysBetween, which is called from main. So the only reason to work on main at this point would be if we wanted to do some manual testing using real user input.
Here is what doing so might look like. This sample is set to compile with a recipe that uses main.cpp and our module, but not the unit tests:
g++ -std=c++20 -fmodules-ts main.cpp DateFunctions.cxx -o program.exe
Listing 12.12.5. main.cpp - using getMonth
You have attempted of activities on this page.