21
$\begingroup$

In C and C++ (and probably other languages), you need to have at least a declaration of functions and fields before you can use them:

#include <iostream>

int main(int argc, char **argv) {
  printout();
  return 0;
}

void printout() {
  std::cout << "TEST" << std::endl;
}
% g++ main.cpp 
main.cpp: In function ‘int main(int, char**)’:
main.cpp:4:3: error: ‘printout’ was not declared in this scope; did you mean ‘printf’?
    4 |   printout();
      |   ^~~~~~~~
      |   printf

Other languages like Java allow you to use methods before declaring them:

public class main {
    public static void main(String[] args) {
    printout();
    }

    private static void printout() {
    System.out.println("TEST");
    }
}
% javac main.java 
% java -cp . main
TEST

What are the advantages at either compile-time or run-time of requiring functions to be declared before they are executed?

$\endgroup$

5 Answers 5

22
$\begingroup$

There is only really one advantage ─ it means that name resolution can be done in a single pass, because names cannot be used before they are declared.

This was more important in the olden days, when programming language design was much more limited by the capabilities of the computers that the compilers had to run on. By requiring forward declarations, one pass was enough to both build a symbol table (i.e. a mapping of names to declarations) and also resolve names to symbols (i.e. when x occurs in an expression, find which declaration of x that name refers to). Then at the end you just needed to check that there were no missing declarations (i.e. forward declarations which weren't later "filled out" with proper declarations).

Nowadays there is no need for this, but you might still do it if the simplicity of implementing your compiler is more important to you than the ease of use of your language; for example, if your compiler is just a hobby project, or you are compiling source code generated programmatically rather than written by humans.

$\endgroup$
1
  • 2
    $\begingroup$ related: Cppfront doesn't require declare-before-use. It generates forward-declarations in the transpiled C++ source. $\endgroup$
    – starball
    May 20, 2023 at 0:18
11
$\begingroup$

@kaya3's answer is correct but incomplete.

Another advantage of forward declarations is that codegen units (CGUs) can be fully independent. For example, compare C's compilation model to Rust's: in C, having the forward declarations for all dependencies is sufficient to compile the CGU, while in Rust there is no equivalent of forward declarations and the compiler must have access to the implementation of the items as well as the declaration.

This has several consequences:

  • It isn't possible to distribute an object file and link against it; the compiler must have access to the sources (this is also true for C++, whose model is more similar to C, but C++ works around it by allowing you to instantiate templates with specific types in the header itself).
  • Building code is much less parallel; Rust crates must form a DAG while C's files are massively parallel since each CGU is fully independent from the others
  • "cyclic imports" are only possible within a CGU. In C it's perfectly possible to have two files which depend on each other's symbols but are compiled independently; in Rust, the requirement for a DAG means that the dependency must be 1-way.

The Rust compiler itself has run into these issues, and in order to improve build times it polyfills forward declarations using function pointers. See https://rustc-dev-guide.rust-lang.org/query.html#providers for more information.

$\endgroup$
10
  • 4
    $\begingroup$ oh, I forgot to mention: Rust's model has one major advantage that C lacks, which is that the compiler can verify that the types are correct. C is happy to let you declare one type for a function in one CGU and another type for that function in a second CGU, and you won't get an error until the code uses the wrong ABI at runtime. $\endgroup$
    – jyn
    May 17, 2023 at 6:43
  • 1
    $\begingroup$ I note that Rust allows cyclic dependencies within a crate (ie, library); only inter-library dependencies must form a DAG. $\endgroup$ Jul 19, 2023 at 8:16
  • $\begingroup$ I am not quite sure about the parallelism argument of C vs Rust. Rust code can be built in parallel too -- that the compiler doesn't is more of a compiler architecture issue. It is easier to parallelize a C build. On the other hand, a parallel C build does more work: each CGU will re-parse every forward declaration. $\endgroup$ Jul 19, 2023 at 8:19
  • 1
    $\begingroup$ the fact that in the cargo/rustc toolchain, cargo spawns 1 rustc process per crate, and the rustc process uses some internal parallelism is very specific to cargo/rustc and not an inherent limitation of internal parallelism. If rustc were used as a library in cargo, you could perfectly have internal parallelism only. There are good reasons for the architecture choice -- it makes developing cargo and rustc independently from one another possible, crashes can be isolated to a single crate, etc... -- but deep integration would likely be faster... $\endgroup$ Jul 20, 2023 at 15:25
  • 1
    $\begingroup$ I am not super familiar with other languages toolchains, unfortunately. I am not sure if asking for a survey of compiler architectures would be on-topic on this site, though it certainly interests me. $\endgroup$ Jul 21, 2023 at 7:26
8
$\begingroup$

Using the same headers for multiple libraries

One advantage of requiring forward declarations is that you can separate out the declarations in a separate file, then swap the actual implementation out at will.

Libraries like curses and glulxe are just header files without any code. You can choose from many different implementations depending on your platform, but they use the same header file and so are guaranteed to be compatible with each other.

You can even compile your project to object files using headers only without having any implementation of the library installed, even though you couldn't run it.

In languages with no forward declarations, it's still possible to check if two libraries follow the same interface, but in languages that have headers, you get that feature for free.

$\endgroup$
2
  • 1
    $\begingroup$ Note this comes with other problems, though - if the header file doesn't match the compiled object file (which can be as subtle as "different -D flags were passed to the preprocessor"), then you can get undefined behavior when your code tries to call it. $\endgroup$
    – jyn
    Jul 20, 2023 at 15:17
  • $\begingroup$ @jyn: It's useful for clients of a library to be able to use a brief description of the interface, without having to use the entire source code of the library. While having a such a digest be a build artifact from compiling the library might be better than having it be a separately maintained source-code file, the risks of such a file becoming out of sync with a compiled library version would seem about the same. $\endgroup$
    – supercat
    Sep 8, 2023 at 20:55
3
$\begingroup$

Pro: Header files provide succinct documentation for the code, compared to the implementation.

Because header files contain only the forward declarations they are far easier to scan through and read than code where the definitions and declarations are unified.

$\endgroup$
2
  • $\begingroup$ I agree this is a benefit. You can get this back with sufficiently good tooling, though — Doxygen and Rustdoc both generate summaries like you're talking about. $\endgroup$
    – jyn
    Jul 20, 2023 at 15:15
  • 1
    $\begingroup$ @jyn: Sure, and many minor languages benefits can be matched by good tooling. Although I will say I have pretty negative experiences with Doxygen (not used Rustdoc), IDE navigation tools also do help but I still find them less useful than header files. That said, I still wouldn't choose header files over other options because IMO the downsides outweigh the benefits. $\endgroup$ Jul 20, 2023 at 15:54
2
$\begingroup$

Advantage: Code readability

When everything has to be declared before use, you can read a source file from beginning to end and you'll never encounter a name you don't know. There's a reason why mathematical texts are usually written this way.

$\endgroup$
2
  • $\begingroup$ I don't think this is true in practice since forward declarations are usually separated into their own header file. $\endgroup$
    – jyn
    Jul 20, 2023 at 15:14
  • $\begingroup$ @jyn Header files are only really a thing in C and C++. $\endgroup$
    – xigoi
    Jul 20, 2023 at 21:56

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .