0

I am a beginner in C++. I'm currently experimenting with the Chromium source code and have noticed the following:

For many of the cc files, there exist an h (header) file which is imported by the cc file. The content of the header file seems to be a set of abstract functions which is fully declared in the cc file.

For example parser.h includes:

  void ParseExportStar(bool* ok);

and parser.cc includes:

#include "src/parsing/parser.h"
.
.
.

void Parser::ParseExportStar(bool* ok) {
  int pos = position();
  Consume(Token::MUL);

  if (!FLAG_harmony_namespace_exports || !PeekContextualKeyword(Token::AS)) {
    // 'export' '*' 'from' ModuleSpecifier ';'
    Scanner::Location loc = scanner()->location();
    ExpectContextualKeyword(Token::FROM, CHECK_OK_VOID);
    .
    .
    .

Is this abstraction? Why is this implementation used and why is the header file even needed? (since the full function is available in the cc file)

3

The header is used so that other modules ( .cc files) can use the function. For example:

foo.h:

int foo();

foo.cpp:

int foo() { 
  // ...
}

bar.cpp:

// Get the declaration of foo()
#include "foo.h"

// Now we can write code that calls foo():
int bar() {
    foo();
}

It's not immediately clear what an "abstract function" necessarily means. It could be used to refer to a function that implements some abstraction (which nearly all functions should). Another possibility I can see is possibly a reference to an abstract base class, which is a base class that contains a pure virtual function.

class foo {
public:
    virtual void bar() = 0; // the "=0" means "pure virtual function"
};

Containing a pure virtual function means we can't instantiate foo--that is, we can't actually create any object of type foo. To create an object, we need to derive another class from foo, and override bar in that derived class:

class baz : public foo {
public:
    void bar() { 
       // ...
    }
};

A virtual member function allows a derived class to override the base class' behavior. A pure virtual requires that the derived class override the base class' behavior to be able to create objects of that class. That, however, is an abstract base class, not an abstract function.

3

C++ has a compilation model where each translation unit is compiled independently. A translation unit is usually each .cc or .cpp source file plus all the headers it includes. Because translation units can be compiled independently, they can also be compiled in parallel or incrementally: after any change I only need to recompile changed translation units. This speeds up compilation of large projects a lot. But note that if a header is changed, all translation units that include the header have to be recompiled.

How can one source file call functions from another file if those files are part of different translation units? The other file might not have been compiled yet! The solution is that the header includes any information necessary so that the compiler can handle a call to that function, without needing to know the implementation of the function. This function declaration includes the function name, the return type, and the argument types. In contrast, a function definition contains the implementation of a function.

The compiler creates object code for each translation unit. Then a linker combines the object code into an executable program. The linker sees when there are function calls between translation units and fixes the addresses in the object code so that the calls go to the correct function. If we called a function that was declared but not defined, the linker shows an error.

There is another kind of error: we might have provided a function definition for the same function in multiple translation units! In C++, this is forbidden due to the One Definition Rule (ODR), which applies to functions and also to global variables. Because each function must be defined once, we usually put the function declaration in a header and put the definition into a corresponding source file. If we had put the complete function definition into the header, it would have been compiled into multiple translation units – an ODR-violation.

Sometimes, it is useful or even necessary to put functions into a header. For example, C++ templates must be fully defined in every translation unit where they are used so that they can be specialized to the template arguments. C++ offers two ways to deal with function in headers:

  • The ODR only applies to functions that are visible outside of this translation unit (have external linkage). If we give the function internal linkage it becomes private to the translation unit. If other translation units have a function of the same name (for example, by including the same header that defines the function), it's completely separate as far as C++ is concerned. We can request internal linkage by declaring a free function static (note that this is different from member functions within a class definition!) or by putting it into an anonymous namespace.

  • Alternatively, we can disable the ODR by defining a function inline. An inline function must be defined in the same translation unit where it was called (possibly in a header). So it doesn't have to link to a different translation unit. If the linker sees definitions for the same inline function in multiple translation units, it doesn't raise an error: by defining a function as inline we promise that all these definitions are identical, so the linker can discard any duplicate definitions.

Unfortunately, headers are not perfect. They have to include far more than only the function declarations we want to make publicly visible. In future C++ versions, modules will allow us to explicitly export specific declarations from a header.

Why don't other languages have to deal with headers?

  • C++ inherited headers from the C language where headers were a kind of performance hack: by dividing the program into smaller compilation units with headers, we can compile a larger program than would fit into memory.
  • C++ mostly lost these performance advantages because C++ programs have much more stuff in their headers, importantly templates from the standard library. These templates like std::vector have to be recompiled in each translation unit.
  • Rust is a modern C++-like language without headers. It has much larger translation units (roughly, one translation unit per library). When a translation unit is compiled, it generates the necessary metadata to allow other code to call into that library. But this also means that there cannot be circular dependencies between translation units (which C++'s headers allow).
  • Many language implementations do not try to compile all the code ahead of time. For example, Java or C#/.NET/CLR only compile to bytecode. At that stage the compiler does some type checking between compilation units but it doesn't perform any linking. This linking only happens at runtime, where information about the complete program is available. Similarly, many dynamic languages like JavaScript or Python have no concept of a compilation phase at all, and any link-like activities happen during run time.

Are headers a kind of abstraction? Usually yes, but not necessarily. They allow us to separate the public interface (like a function declaration) from internal details (like a function definition). Unfortunately, C++ often makes it necessary to put “private” information into the header (such as private class members). This is because the header does not only describe an Application Programming Interface (API) but also an Application Binary Interface (ABI) – not just which calls are allowed but exactly how those calls will be performed, down to the memory layout of all function arguments. To abstract over such implementation details as well, we would have to apply object-oriented techniques ourselves.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.