Understanding Preprocessor Directives in C++ as a beginner

Viewed 172

I recently began learning C++. As a programmer coming from Python, I've noticed some general similarities when it comes to how certain things in C++ do the same thing over in Python.

One question I had is understanding Preprocessor directives. I/O Stream seems to be a common one to use in beginner programs.

Is #include effectively the same thing as import in Python, or is it completely different than importing "modules"?

2 Answers

C++ did not have modules until the latest standard (C++20). #include is not the same as import in the languages that support modules. Instead, it is a source code - level inclusion of a "header" file. Usually, the headers only contain declarations but not definitions of what you are "importing". The definitions are contained in compiled libraries that are added by the linker.

Congrats on diving in to C++, you're going to have many more questions and confusions coming from Python, especially if you use some of the newer standards (like C++11/14/17/20).

That aside, answering your question directly:

Is #include effectively the same thing as import in Python or is it completely different than importing "modules."

I won't speak to C++20 modules as that functionality is not fully supported across the various compilers and that is not your question. Unfortunately the answer is not a simple yes or no, it's kind of both.

In C and C++, the #include pre-processor directive essentially does a "copy-paste" of whatever file you #include before it does the compilation stage. This allows you to separate large chunks of code into easier to manage files and still reference the code in said file.

In Python/C#/Java and various other languages, you don't #include a file you want to access the classes and functions of, you import the namespace or module you wish to reference and the JIT compiler "knows" which file that module or namespace is in, allowing you to use the functionality of the code in that file.

Python and C++ don't "build" the code in the same way and thus don't reference parts of the source code in the same way.

To illustrate this point more succinctly, take the following C++ code:

file: fun.hpp

#define FUN_NUM 1
namespace fun
{
    int get_fun()
    {
        return FUN_NUM;
    }
}

file: main.cpp

#include <iostream>
#include "fun.hpp"

int main(int argc, char* argvp[])
{
    if (fun::get_fun() == FUN_NUM) {
        std::cout << "Fun!" << std::endl;
    }
    return FUN_NUM;
}

In the above code, when we #include "fun.hpp", what the C++ pre-processor does before compiling is essentially "copy-and-paste" the code in iostream and fun.hpp, so what actually gets compiled is something like the following:

file: main.cpp

// #include <iostream> <- this is replaced with the whole std::iostream file
// not putting that here as it's huge.

// #include "fun.hpp" <- this is replaced with this:
#define FUN_NUM 1
namespace fun
{
    int get_fun()
    {
        return FUN_NUM;
    }
}

int main(int argc, char* argvp[])
{
    if (fun::get_fun() == FUN_NUM) {
        std::cout << "Fun!" << std::endl;
    }
    return FUN_NUM;
}

It is because of this "copy-paste" that you also need to have include guards, because if you did something like the following:

file: main.cpp

#include <iostream>
#include "fun.hpp"
#include "fun.hpp"

int main(int argc, char* argvp[])
{
    if (fun::get_fun() == FUN_NUM) {
        std::cout << "Fun!" << std::endl;
    }
    return FUN_NUM;
}

This code won't compile because you'll get errors about various items being redeclared since what gets compiled is the following:

file: main.cpp

// #include <iostream> <- this is replaced with the whole std::iostream file
// not putting that here as it's huge.

// #include "fun.hpp" <- this is replaced with this:
#define FUN_NUM 1
namespace fun
{
    int get_fun()
    {
        return FUN_NUM;
    }
}
// #include "fun.hpp" <- this is replaced with this:
#define FUN_NUM 1
namespace fun
{
    int get_fun()
    {
        return FUN_NUM;
    }
}

int main(int argc, char* argvp[])
{
    if (fun::get_fun() == FUN_NUM) {
        std::cout << "Fun!" << std::endl;
    }
    return FUN_NUM;
}

To protect from the double inclusion and redefinition, you can simply do something like the following:

file: fun.hpp

#if !defined(FUN_HPP)
#define FUN_HPP
#define FUN_NUM 1
namespace fun
{
    int get_fun()
    {
        return FUN_NUM;
    }
}
#endif // define FUN_HPP

So unless you pass FUN_HPP as a pre-processor define to the compiler, then FUN_HPP will not be defined until the file is #include'd once, then any other times it's included, FUN_HPP will already be defined and thus the pre-processor will not include the code again, ridding the problem of double-definitions.

So where your question is concerned, the #include directive in C++ is somewhat like the import directive in Python, but mostly to the effect that they both allow the file you are putting that directive in, to access code more directly from that import or #include.

I hope that can add a little clarity.

Related