CMake & third-party libraries

Recap

Terminologies

  • target: An output or result that the build system generates, can be an executable, a library, an object file, etc.

Functions to understand:

  • add_library(<name> [<type>] <sources>...): Add a library target called <name> to be built from the source files listed.
    • <type> can be SHARED or STATIC, corresponding to dynamic and static library.
    • <sources> are the source files to be compiled, separated by spaces. The path is relative to the current CMakeLists.txt file.
  • target_link_libraries(<target> <libraries>...): Specify the libraries that the target depends on.

Library

Suppose the file structure is

├── CMakeLists.txt
├── answer.cpp
├── answer.hpp
└── main.cpp

The content of CMakeLists.txt is

cmake_minimum_required(VERSION 3.10)
project(answer)

add_library(libanswer STATIC answer.cpp)
add_executable(answer main.cpp)
target_link_libraries(answer libanswer)

This will generate a static library under the build directory, and the executable will link to this library.

Subdirectory

add_subdirectory(<dir>)

The previous example is still not good enough when we have nested subdirectories or multiple libraries.

  • add_subdirectory(<dir>): Add a subdirectory to the build.
  • Typically we put another CMakeLists.txt in <dir> directory.
  • add_subdirectory(<dir>) tells CMake to process the CMakeLists.txt in <dir>.
.
├── CMakeLists.txt
├── answer
│   ├── CMakeLists.txt
│   ├── answer.cpp
│   └── answer.hpp
└── main.cpp

CMakeLists.txt in answer directory:

add_library(libanswer answer.cpp)
target_include_directories(libanswer PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

CMakeLists.txt in the root directory:

cmake_minimum_required(VERSION 3.10)
project(answer)

# subdirectory
add_subdirectory(answer)

add_executable(answer_app main.cpp)
target_link_libraries(answer_app libanswer)

Access control

PUBLIC, PRIVATE, INTERFACE

  • target_include_directories(<target> PUBLIC [dir]):
    • You can change PUBLIC to PRIVATE or INTERFACE to control the access.
    • PUBLIC: The include directories are propagated to dependent targets.
    • PRIVATE: The include directories are not propagated to dependent targets.
    • INTERFACE: The include directories are only used in dependent targets, current target will not use them.
  • This command takes effect during compile process.
  • Normally we don’t need to care about these, just use PUBLIC to propagate the include directories.

target_link_libraries(<target> <PUBLIC> <item>):

  • This command takes effect during link process.
  • PUBLIC, PRIVATE, INTERFACE control whether the library is propagated to dependent targets.

Suppose A depends on B, and B depends on C.

target_link_libraries(D PUBLIC B)
target_link_libraries(A PRIVATE B)
target_link_libraries(B PUBLIC C)
  • Any target that links to B will automatically link to C as well.
  • D links against B using PUBLIC, then D will link to C as well.
  • However, A links against B using PRIVATE, then A will not link to C.

Third-party libraries

Pure header library

This is the easiest way to use third-party libraries.

  • Download the library from the internet.
  • Copy the header files to your project.
  • Add the header files to your project.

Using add_subdirectory

Recall that add_subdirectory(<dir>) will add a subdirectory to the build and process the CMakeLists.txt in the subdirectory. This means we can download source codes from the internet and put them in a subdirectory.

Suppose we want to use fmt library, we can use git to download the source code, in your project directory run:

git clone https://github.com/fmtlib/fmt.git fmt --depth 1

Then you will find a fmt directory in your project.

To use the fmt library,

  • add_subdirectory(fmt): Process the CMakeLists.txt in the fmt directory.
    • you don’t need to worry about the CMakeLists.txt in the fmt directory.
cmake_minimum_required(VERSION 3.10)
project(fmt_example)

add_subdirectory(fmt)
add_executable(hello_fmt main.cpp)

target_link_libraries(hello_fmt PUBLIC fmt::fmt)

Here fmt::fmt is for the compiled library, another option is fmt::fmt-header-only, which is the header files only. See here for more details.

fmt library

Here are some useful cases of using fmt library:

#include <fmt/core.h>

int main() {
  fmt::print("Hello, world!\n");
}

std::string s = fmt::format("The answer is {}.", 42);
// s == "The answer is 42."

#include <vector>
#include <fmt/ranges.h>

int main() {
  std::vector<int> v = {1, 2, 3};
  fmt::print("{}\n", v);
}

For more details, read this.

Assignment

Implement a Complex class that overloads +, - operators.

  • Two private members: real and imaginary of type double.
  • Constructor that takes two double numbers, real and imaginary.
    • By default, real is 0 and imaginary is 0.
  • Overload +, - operators that support:
    • Complex + Complex and Complex - Complex
    • Complex + double or double + Complex
    • Complex - double or double - Complex

Read the document and use fmt library to format the output of the complex number.

  • Overload << operator for printing the complex number.
    • The color of the complex number should be green.
    • The format of the output should be a + bi, where a is the real part and b is the imaginary part. Two spaces between + and b.
    • The precision of the real and imaginary parts should be 3.
Complex c1(1.1243, 2.3456);
Complex c2(3.234, 4.234);
std::cout << c1 << std::endl; // Output: 1.124 + 2.346i
std::cout << c2 << std::endl; // Output: 3.234 + 4.234i
  • Please use const everywhere if you don’t change the data member or passed parameters.
  • Pass by reference if you can.
  • You should write complex.h, complex.cpp, main.cpp and CMakeLists.txt by yourself.
    • Write your own test cases in main.cpp to test your Complex class.
  • Only submit complex.h, complex.cpp, main.cpp and CMakeLists.txt. i.e. delete the fmt and build folder before submission.

Example test case:

Complex c1(1.1243, 2.3456);
Complex c2(3.234, 4.234);
std::cout << "c1: " << c1 << std::endl;
std::cout << "c2: " << c2 << std::endl;

Expected output: