Naetw/Naetw.github.io

How C++ Works: Understanding Compilation

Naetw opened this issue · 0 comments

Naetw commented

How C++ Works: Understanding Compilation

Translation Unit (Compilation Unit)

  • A translation unit is the basic unit of compilation in C++. It consists of the contents of a single source file, plus the contents of any header files directly or indirectly included by it, minus those lines that were ignored using conditional preprocessing statements.
  • C++ 編譯過程中的基本單位,組成為原始碼 + 裡面有 include 進來的所有 header(不論是直接 include 還是間接 include 都有)並移掉由 conditional preprocessing statement 中不會走到的程式碼。簡單來說就是 preprocessor 處理後的內容。

Name Mangling (Name Decoration)

  • 一種用來解決由於程式實體中名字必須唯一而導致的問題的技術,它提供了一種方式來加入額外的資訊在 function, structure, class name 中,這使得編譯器可以傳遞更多語意資訊給連結器。
  • 使用時機在於當程式語言允許不同的程式實體但是卻用相同的識別字時,像是:
    • different namespace
    • different signature (e.g., function overloading)
  • main 是 implementation-defined 的函式,也就是不同編譯器有不同的結果(可能 mangle 也可能沒有)。

extern "C"

  • 讓 C++ 的 function/variable name 可以擁有 C 的連結方式,也就是編譯器不會對它做 mangling,如此一來 C 的程式才能夠連結(使用)到 C++ 的 function/variable。
  • 延伸閱讀:
    • Ref
      • [Pending] 文中還有 language linkage 跟 external linkage,不太清楚定義
    • [Pending] How to mix C and C++

Sample print.cpp:

#include <iostream>
#include "sum.h" // sumI, sumF

void printSum(int a, int b) {
  std::cout << a << " + " << b << " = " sumI(a, b) << std::endl;
}

void printSum(float a, float b) {
  std::cout << a << " + " << b << " = " sumF(a, b) << std::endl;
}

extern "C" void printSumInt(int a, int b) {
  printSum(a, b);
}

extern "C" void printSumFloat(float a, float b) {
  printSum(a, b);
}

上面程式中,printSum 是一個 overloading function,也就是沒辦法直接被 C 使用,為了給 C 使用,可以用 wrapper function 像是上面的 printSumInt & printSumFloat,並且將它們用 extern "C" 去標註,好讓編譯器不會對他們做 mangling。接著用 nm 來看看 symbols:

$ g++ -c print.cpp
$ nm print.o
0000000000000132 T printSumFloat
0000000000000113 T printSumInt
                 U sumF
                 U sumI
0000000000000074 T _Z8printSumff
0000000000000000 T _Z8printSumii
                 U _ZSt4cout
  • T 代表位於 .text 區段。
  • 可以看到用 extern "C" 定義的 wrapper function 並沒有被編譯器給 mangled。
  • U 代表 Undefined。可以看到 Undefined 的是 sumF, sumI, & std::cout 這些在目前的 source file print.cpp 中並未定義,要在之後 linking 中跟別的 object file 或是 libraries 連結在一起才能夠使用。

接著來看看如何設計一個可以給 C & C++ include 的標頭檔,目標是讓 printSumInt & printSumFloat 可以被 C & C++ 呼叫,而 printSum 可以被 C++ 呼叫:

#ifdef __cplusplus
void printSum(int a, int b);
void printSum(float a, float b);
extern "C" {
#endif

void printSumInt(int a, int b);
void printSumFloat(float a, float b);

#ifdef __cplusplus
} // end extern "C"
#endif
  • __cplusplus:通常 C++ 編譯器中會自己定義這個 macro,因此不需要特地加 flag。
  • 由於 printSumInt & printSumFloat 是 C++ code,若要用 C code 來呼叫並編譯成可執行檔必須使用 C++ 的 linker,像是 g++ -o c-app sum.o print.o c-main.o

Include Guards

#ifndef __GUARDED_HPP
#define __GUARDED_HPP

... 

#endif // __GUARDED_HPP
  • Include guard 只會避免在同一份原始碼中重複宣告(也就是重複 include 相同標頭檔),不同檔案還是會 include。

Forward Declaration

這邊指的是跟 compiler 比較相關議題。

  • 可以用來避免不必要的編譯動作,在大型專案中常用來減少整體的編譯時間。
    • 減少因為 #include 而被開啟的檔案數,也就減少了需要的系統呼叫次數。
    • 減少檔案大小,因為沒有把整個標頭檔塞進來。
    • 減少重複編譯的問題。
      • 舉例來說,今天有個 a.hb.cpp,假設 b 只用到 a 中的某個類別或是某個函式,但是 a 中還有其他很多東西,若某天 a.h 做了很大的更動,但是 b 使用到的部分並沒有更動,那麼這時候如果是使用 #include 來使用 a.h 內的東西,b.cpp 就會需要重新編譯,但是實際上 b.cpp 使用到的部分根本跟之前一樣;若是使用 forward declaration 只有將需要用到的部分直接在 b.cpp 中宣告,那麼就可以避免這個重複編譯的問題。
  • 有使用時機的限制,可以參考延伸閱讀的「When Can I Use a Forward Declaration」。
  • 延伸閱讀:

MISC Stuff

  • nm 可以列出檔案中的 symbols
    • -C 可以 demangle。
  • using namespace std 不要放在標頭檔中,因為一旦被插入到原始碼中,後面的標頭檔也會同時擁有該 scope 底下的所有 symbols。這會造成一些名稱的混亂,畢竟對其他標頭檔來說預設可能是沒有這個 directive 的。
  • [Pending] const correctness
  • [Pending] 文中提到有些函式宣告一些函式內部不會使用到的參數是跟 interface 有關,像是 callback interface,不是很懂,有空再研究。
    • 原文 "Surprisingly, variable names aren’t either needed in the definition of a function. They are only needed if you actually use the parameter in the function. But if you never use it you can leave the parameter with the type but without the name. Why would a function declare a parameter that it’d never use? Sometimes functions (or methods) are just part of an interface, like a callback interface, which defines certain parameters that are passed to the observer. The observer must create a callback with all the parameters that the interface specifies, since they’ll be all sent by the caller. But the observer may not be interested in all of them, so instead of receiving a compiler warning about an “unused parameter,” the function definition can just leave it without name."
  • [Pending] Argument Passing in C++, move semantics
  • [Pending] One-Pass-Compiler, Multi-Pass-Compiler