CMake2

Recap

Why multiple files?

It’s convenient to put all codes in one file, but it has some disadvantages:

  • All the codes are mixed together, making it hard to read and maintain.
  • When your project becomes larger, the time to compile the code becomes longer.
    • Even if you only modify one file, you have to compile the whole project.

How to separate codes into multiple files?

Before: one source file.

// main.cpp
#include <iostream>

void print_hello() {
    std::cout << "Hello, world!" << std::endl;
}

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

After: 1 header file, 2 source files.

// hello.h
#pragma once

void print_hello();
// hello.cpp
#include "hello.h"
#include <iostream>

void print_hello() {
    std::cout << "Hello, world!" << std::endl;
}
// main.cpp
#include "hello.h"

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

After we separate the codes into multiple files, we can compile the project faster.

  • If we only modify hello.cpp, we only need to compile hello.cpp.
  • If we modify main.cpp, we only need to compile main.cpp.
  • If we modify the function prototype in hello.h, we have to recompile all the source files that include hello.h.
    • In our case, we have to recompile both hello.cpp and main.cpp.

How to compile multiple files?

Use g++ as example, we can compile multiple files by:

  1. Compile the source files into object files.

    g++ -c hello.cpp -o hello.o
    g++ -c main.cpp -o main.o
  2. Link the object files into an executable file.

    g++ hello.o main.o -o main

Makefile

Pros:

  • As long as you define the rules in Makefile, it will automatically compile the project.
  • Make will atuomatically decides which files to recompile by checking the dependencies.

Cons:

  • Makefile requires you to write rules manually.
  • Different compilers may have different syntax, you can’t use one Makefile for different compilers.

CMake

CMake is a build system generator, it generates native build scripts for various compilers.

  • CMake is platform-independent, you can use one CMake project for different operating systems and different compilers.
  • CMake can detect the relationship between source files and header files automatically.
  • It’s easier to write than Makefile, and convenient to include third-party libraries.

A simple example of CMakeLists.txt, we still suppose we have hello.h, hello.cpp and main.cpp.

cmake_minimum_required(VERSION 3.10)
project(Hello)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_executable(main main.cpp hello.cpp)

To compile and run the project,

cmake -B build -S .
cmake --build build
./build/main

Library

Why use library?

  • Sometimes we have multiple executables, they may share some common parts of codes.
    • We pack these common parts into a library, and then link them when compiling executables.
  • The functions in the library can be used by executables, also other libraries.

Static library

  • After compilation, the object code of the library is included in the executable file.
  • Easy to use, but the final executable file will become larger.

In CMake, we can create a static library by:

   CMakeLists.txt
   main.cpp

└───hello
        CMakeLists.txt
        hello.cpp
        hello.h
# CMakeLists.txt under hellolib
add_library(hellolib STATIC hello.cpp)
target_include_directories(hello PUBLIC .)
# CMakeLists.txt under main
cmake_minimum_required(VERSION 3.10)
project(hellocmake)

add_subdirectory(hellolib)

add_executable(main main.cpp)
target_link_libraries(main PUBLIC hellolib)

CMakeLists.txt under hellolib directory

  • add_library(hellolib STATIC hello.cpp) tells CMake to build a library hellolib from the source file hello.cpp.1
    • STATIC means a static library.
  • target_include_directories(hellolib PUBLIC .):
    • specifies include directories to use when compiling the target hellolib.
    • . means the current directory.
    • PUBLIC means when other targets link to hellolib, they will also use the include directories of hellolib.

Handle header files

target_include_directories(hellolib PUBLIC .) is crutial in our project.

We want to aovid changing the include directive to

// main.cpp
#include "hello/hello.h"

With the help of this command, no need to change.

// main.cpp
#include "hello.h"

We can even use the following2

// main.cpp
#include <hello.h>

CMakeLists.txt under main directory

# CMakeLists.txt under main
cmake_minimum_required(VERSION 3.10)
project(hellocmake)

add_subdirectory(hellolib)

add_executable(main main.cpp) # no need to specify hello.cpp
target_link_libraries(main PUBLIC hellolib)
  • add_subdirectory(hellolib) tells CMake to add the subdirectory into the project.
    • CMake will execute the CMakeLists.txt in the hellolib directory.
  • target_link_libraries(main PUBLIC hellolib) tells CMake to link the library hellolib to the executable main.
    • PUBLIC means when other targets link to main, they will also link to hellolib.3

Dynamic library

  • After compilation, the object code of the library is not included in the executable file.
    • The executable file contains “stub” to find and load the library at runtime.
  • At runtime
    • .dll or .so files are loaded into memory.
    • The “stub” in the executable file will find the library in memory and load it.
  • Dynamic library is easier to update, everytime you update the library, you don’t need to recompile the executable file. It’s also harder to use and debug.

To create a dynamic library, we can modify the CMakeLists.txt in hellolib directory to:

add_library(hellolib SHARED hello.cpp)
target_include_directories(hello PUBLIC .)

What we modified is just changing STATIC to SHARED.

Assignment

Word Abbreviation: Given a word, output its abbreviation.

Rules:

  1. If the word has more than 3 characters:
    • Keep the first character
    • Count the number of characters between the first and last character
    • Add this number after the first character
    • End with the last character of the original word
  2. If the word has 3 or fewer characters, leave it unchanged

Examples:

  1. kubernetes -> k8s
  2. localization -> l10n
  3. internationalization -> i18n
  4. accessibility -> a11y
  5. documentation -> d11n
  6. dog -> dog (unchanged)
  7. do -> do (unchanged)

You can assume that the length of the input word will not exceed 100.

Requirements:

  1. Download the zip file and configure the project with CMake.
  2. Implement the function std::string abbreviate(const std::string& word) in abbreviate.cpp.
  3. Test cases are given in main.cpp.

CMake Assignment

Given the assignment2 folder, write multiple CMakeLists.txt to compile the project.

  1. CMakeLists.txt under payoff:
    • Create a lib called payofflib.
  2. CMakeLists.txt under random:
    • Create a lib called randomlib.
  3. CMakeLists.txt under root folder of project:
    • Compile the project and link the two libraries, the executable file should be named main.

Bonus (5 points)

For the first assignment:

  • Implement the function using C-style strings instead of std::string.
  • Function declaration: void abbreviateWord(char* word)
  • Requirements:
    1. Use only control flows and string functions discussed in class (e.g., strlen, strcpy).
    2. Assume the input char array has a maximum size of 100 characters.
    3. Modify the string in-place (don’t create a new string).
    4. Ensure the string is null-terminated after modification, set any unused characters in the array (after the null terminator) to ‘\0’.

Footnotes

  1. You can list multiple source files if the library is complex.↩︎

  2. Because the path in target_include_directories(hellolib PUBLIC .) will be treated as system path.↩︎

  3. In our case, main is executable file, so it doesn’t matter.↩︎