The Preprocessor

Before C++ programs are compiled, they go through the “preprocessor,” which evaluates bits of code that begin with a “#” (such as “#include“). There are lots of things you can do with the preprocessor, but I’ll only cover some of the most useful applications. Click here for a more complete guide.

#define

Using the “#define” preprocessor directive, we can create “macros” that are given a specific value. Any time that macro appears later in our code, the preprocessor replaces it with the value it represents. Below, we create a macro called HELLO for the string “Hello!”:

#include <iostream>

#define HELLO "Hello!"

int main() {
    std::cout << HELLO << "\n";
    return 0;
}

Program output:

Hello!

(Usually, macros should be typed in all capital letters, just like const variables.)

Macros can very easily be abused, however:

#include <iostream>

#define MAIN int main() {
#define PRINT std::cout <<
#define HELLO "Hello!" 
#define NEWLINE << "\n";
#define EXIT return 0; }

MAIN
    PRINT HELLO NEWLINE
EXIT

Program output:

Hello!

Most of the time, we’ll use macros that aren’t even given a value. Then we can use preprocessor if statements to check if those macros have been “defined” previously:

#include <iostream>

#define USE_FUNCTION_1

void function1() {
    std::cout << "Function 1\n";
}

void function2() {
    std::cout << "Function 2\n";
}

int main() {
    #ifdef USE_FUNCTION_1
        function1();
    #endif

    #ifdef USE_FUNCTION_2
        function2();
    #endif

    return 0;
}

Program output:

Function 1

#ifdef” means “if the following word has been previously defined.” Any code following that (up until the “#endif“) is deleted by the preprocessor if the word has not been defined. Therefore, after the above program is preprocessed, the main function will look something like this:

int main() {
    function1();
    return 0;
}

function2(); is removed from the code because USE_FUNCTION_2 is not defined. However, USE_FUNCTION_1 is defined, so function1(); is not removed from the code.

The opposite of #ifdef is #ifndef which checks if the following word is not defined. I’ll show an example of this later on.

Use Cases

Most of the time, macros are not the best solution to a problem because they can make code a bit harder to read. However, they can be very useful when testing a program out. Sometimes I may write code to test if a function is working properly (e.g. by printing some data to the screen). Then, I can have that code only run if some macro (“TEST” for example) is defined. By doing this, I can very quickly define the macro (or remove the definition) to control when to run the testing code.

You’ll see below how macros can also be useful with #include.

#include

When the preprocessor sees #include in your code, it finds the file you’re including and pretty much copies and pastes that file into your program. That way, the rest of your program has access to features from that file.

We often use #include to separate our own programs into multiple files for better organization. It’s most commonly used to place each class in its own file. To do so, we usually separate a class’s “interface” from its “implementation.”

  • Interface: the general “shell” of our program (e.g. function and variable names)
  • Implementation: the actual functionality of our program (e.g. what different functions actually do)

A class’s interface is usually placed in a file with a “.h” or “.hpp” extension, while implementation is placed in a file with a “.cpp” extension.

  • All “.cpp” files are compiled by the compiler and “linked” together.
  • “.h” and “.hpp” files (“header” files) are generally what we “#include” in our programs.

An example will help to show this:

Motor.h:

#ifndef MOTOR_H
#define MOTOR_H

#include <iostream>
#include <string>

class Motor {
    private:
        std::string name;
    
    public:
        Motor(std::string name);
        void start(float speed);
        void stop();
};

#endif

Motor.cpp:

#include "Motor.h"

Motor::Motor(std::string name) {
    this->name = name;
}

void Motor::start(float speed) {
    std::cout << "Starting motor " << name << " with speed: " << speed << "\n";
}

void Motor::stop() {
    std::cout << "Stopping motor " << name << "\n";
}

main.cpp:

#include "Motor.h"

int main() {
    Motor frontLeftMotor("Motor0");
    frontLeftMotor.start(0.7);
    frontLeftMotor.stop();
    return 0;
}

Program output:

Starting motor Motor0 with speed: 0.7
Stopping motor Motor0

Note: To properly compile this program using Visual Studio Code, go to your tasks.json file and find:

"args": [
    "-g",
    "${file}",
    "-o",
    "${fileDirname}\\${fileBasenameNoExtension}.exe"
],

From there, change “${file}” to “${fileDirname}\\**.cpp” . This ensures that every file in your project is compiled (not just one of them).

Code Walk-through

Let’s start with Motor.h:

This file begins with a “header guard” that ensures the file is not included multiple times in a single file. Without a header guard, you could accidentally end up with multiple variables/functions/classes of the same name in a single file, which would break your program. Here’s how header guards work:

  • We use a macro that is related to the file name (MOTOR_H in this case).
  • All code inside the header file is deleted by the preprocessor if that macro has already been defined.
  • If the macro has not been defined, that means the following code is not already included in the current file.
    • Therefore, the macro is defined, and the code can be included in the file.
  • The #ifndef statement ends with #endif at the end of the file.

The rest of the header file consists of the Motor class’s interface: just variable and function names.

Motor.cpp:

We start by including the Motor.h file. Because this file is inside of our current project folder (unlike files like “iostream” and “string”), we use quotes around the file name, rather than brackets (“<” and “>”). The Motor.h file needs to be included in order to access the names of variables and functions within the Motor class.

To implement each function in the Motor class, we need to prefix each function with “Motor::“. This lets the computer know that we’re referring to the function that is inside the Motor class rather than just creating a new function of that name.

main.cpp:

We start by including the Motor.h file in order to access the names of variables and functions within the Motor class. Here’s what main.cpp would look like after being preprocessed:

iostream file contents here...

string file contents here...

class Motor {
    private:
        std::string name;
    
    public:
        Motor(std::string name);
        void start(float speed);
        void stop();
};

int main() {
    Motor frontLeftMotor("Motor0");
    frontLeftMotor.start(0.7);
    frontLeftMotor.stop();
    return 0;
}

As I briefly mentioned earlier, all “.cpp” files are “linked” together after being compiled, so main.cpp automatically has access to Motor.cpp. Therefore, just by including the header (“.h”) file, we can use everything from the Motor class.

Application

When we start robotics programming, all of our classes will be placed in their own header and “.cpp” files. This way, rather than scrolling through hundreds of lines of code to find a single class, we can just find its own file.

Also, be aware that separating implementation from interface (like we did above) is not required. In fact, all of the Motor class’s code could have just been placed in its header file. However, that’s not the conventional way of using multiple files, so it should be avoided.

(Why is this the convention? It’s generally because, by separating implementation from interface, code becomes slightly more structured.)

Challenge Problem

Create the most ridiculous program you can by (ab)using macros. That’s all.