| marp | true |
|---|---|
| math | katex |
| theme | custom-theme |
| footer |
- Different types of libraries
- Header-only
- Static
- Dynamic
- What is linking
- When to use the keyword
inline - Some common best practices
📺 Watch the related YouTube video!
- 🎨 - Style recommendation
- 🎓 - Software design recommendation
- 😱 - Not a good practice! Avoid in real life!
- ✅ - Good practice!
- ❌ - Whatever is marked with this is wrong
- 🚨 - Alert! Important information!
- 💡 - Hint or a useful exercise
- 🔼1️⃣7️⃣ - Holds for this version of C++(here,
17) and above - 🔽1️⃣1️⃣ - Holds for versions until this one C++(here,
11)
Style (🎨) and software design (🎓) recommendations mostly come from Google Style Sheet and the CppCoreGuidelines
Let's say we implement a new machine learning framework 😉
#include <vector>
#include <iostream>
[[nodiscard]] int
PredictNumber(const std::vector<int>& numbers) {
// Arbitrarily complex code goes here.
if (numbers.empty()) { return 0; }
if (numbers.size() < 2) { return numbers.front(); }
const auto& one_before_last = numbers[numbers.size() - 2UL];
const auto difference = numbers.back() - one_before_last;
return numbers.back() + difference;
}
// Many more similar functions.
int main() {
const auto number = PredictNumber({1, 2});
if (number != 3) {
std::cerr << "Our function does not work as expected 😥\n";
return 1;
}
return 0;
}- For now code lives in a single binary
- Now assume that we have two programs we want to write:
- One to predict the house pricing
- One to predict the bitcoin price
- These should use our "machine learning" functions
- And other things special for those usecases
- 😱 Should we just copy the code over?
- Our code is duplicated
- If we have more binaries, we have more copies
- Any changes for the functionality needs to be synced
- It requires us to keep this in mind - which is error prone
- (violates the DRY principle)
ml.h
#pragma once // Stay tuned 😉
#include <vector>
[[nodiscard]] inline // Stay tuned for "inline"
int PredictNumber(const std::vector<int>& numbers) {
// Compute next number (skipped to fit on the slide)
return next_number;
}predict_housing.cpp
#include <ml.h>
#include <iostream>
int main() {
const auto prices =
MagicallyGetHousePrices();
std::cout
<< "Upcoming price: "
<< PredictNumber(prices);
return 0;
}predict_bitcoin.cpp
#include <ml.h>
#include <iostream>
int main() {
const auto prices =
MagicallyGetBitcoinPrices();
std::cout
<< "Upcoming price: "
<< PredictNumber(prices);
return 0;
}- All functions are implemented in header files (
.h,.hpp) - We
#includethese header files in our binaries - 💡 Put your includes first, then other libraries, then standard
Pros:
- Compiler sees all code so it can optimize it well
- Compilation remains simple (just need the new
-Iflag)c++ -std=c++17 -I folder_with_headers binary.cpp
Cons:
- We always recompile the code in headers
- Changes in headers require recompilation of depending code
- If we ship the code it remains readable to anyone
- We should make the functions
inline(stay tuned)
- A preprocessor directive that ensures the header in which it is written is only included once
- There are compilers that don't support it, but most do
- Alternative --- include guards
For file
file.hinfolder/they can be:
#ifndef FOLDER_FILE_H_ #define FOLDER_FILE_H_ #endif /* FOLDER_FILE_H_ */
- They also ensure the header file will be included only once
- ✅ Always use one of these in your header files!
- Move only declarations to header files:
*.hor*.hpp - Move definitions to source files:
*.cppor*.cc - Compile corresponding source files to object files
- Bind the object files into libraries
- Link the libraries to executables
- The library is built once, and linked to multiple targets!
- If we change the code in a library we only need to:
- Rebuild only the library
[maybe]Relink this library to our executables
Declaration: ml.h
#pragma once
#include <vector>
[[nodiscard]] int
PredictNumber(const std::vector<int>& numbers);Definition: ml.cpp
#include <ml.h>
#include <vector>
[[nodiscard]] int
PredictNumber(const std::vector<int>& numbers) {
// Compute next number (skipped to fit on the slide)
return next_number;
}Calling it: predict_prices.cpp:
#include <ml.h>
#include <iostream>
int main() {
const auto prices = MagicallyGetBitcoinPrices();
std::cout << "Upcoming price: " << PredictNumber(prices);
return 0;
}c++ -std=c++17 predict_prices.cpp -I . -o predict_pricesUndefined symbols for architecture arm64:
"PredictNumber(
std::__1::vector<int, std::__1::allocator<int> > const&)",
referenced from: _main in predict_prices-066946.o
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1
(use -v to see invocation)💡 Your error will look similar but slightly different
c++ -std=c++17 -I . ml.cpp predict_prices.cpp❌ not really - does not solve our "recompilation" issue!
- Compile source files into object files (use the
-cflag)
c++ -std=c++17 -c ml.cpp -I includes -o ml_static.o
Assuming that all includes live in thec++ -std=c++17 -c -fPIC ml.cpp -I includes -o ml_dynamic.o
includesfolder, results in*.obinary files that an OS can read and interpret - Pack objects into libraries:
- Static libraries (
*.a) are just archives of object filesar rcs libml.a ml_static.o <other_object_files>
- Dynamic libraries (
*.so) are a bit more complexc++ -shared ml_dynamic.o <other_object_files> -o libml.so
- Static libraries (
- Finally, we link the libraries to our binary
- Linking tells the compiler in which binary library file to find the definition for a declaration it sees in a header file
- Link our
mainexecutable to the libraries it usesc++ -std=c++17 main.cpp -L folder -I includes -lml -o main
-I includes- Headers are in theincludesfolder-L folder- Addfolderto the library search path-lml- Link to the library filelibml.aorlibml.so- 🚨 Note that
-lflags must be after all.cppor.ofiles - 🚨 Same usage for both static and dynamic libraries but a different resulting executable
- Static libraries are copied inside the resulting
mainbinary - Dynamic libraries are linked to the resulting
mainbinary
- Binaries with static linkage:
- Contain binary code of other libraries, usually bigger
- Can be copied anywhere on any similar operating system
- Binaries with dynamic linkage:
- Contain references to other libraries, usually smaller
- Dependencies (dynamic libraries) are looked up at runtime
- Relative to the current path
- In the paths stored in
LD_LIBRARY_PATHvariable
- If you move your binary or libraries you might break it
- See linked libs with
ldd(Linux) orotool -L(MacOS)
- In this course we will use static libraries
- Let's assume we have a header file
print.h:#include <iostream> // Notice no inline keyword here void Print(const std::string& str) { std::cout << str << "\n"; }
- We use this file in two compiled libraries:
fooandbar
foo.h
void Foo();foo.cpp
#include <print.h>
void Foo() {
Print("Foo");
}bar.h
void Bar();bar.cpp
#include <print.h>
void Bar() {
Print("Bar");
}- Finally, we write a program that uses them:
main.cpp#include <foo.h> #include <bar.h> int main() { Foo(); Bar(); return 0; }
- And compile it as an executable
main:c++ -std=c++17 -c -I . main.cpp -o main.o c++ -std=c++17 -c -I . foo.cpp -o foo.o ar rcs libfoo.a foo.o c++ -std=c++17 -c -I . bar.cpp -o bar.o ar rcs libbar.a bar.o c++ main.o -L . -I . -lfoo -lbar -o main
❌ Oops, it does not link! (build it to see the error 😉)
- Linker failed because we violated ODR --- One Definition Rule
- It states that there must be exactly one definition of every symbol in the program, i.e., your functions and variables
- We have two libraries
libfoo.aandlibbar.awith source files that both include theprint.hand therefore have a definition of thePrint(...)function - Our executable links to both
libfoo.aandlibbar.a, so it has two definitions for thePrint(...)function, which causes an error ❌
- ODR allows to have multiple definitions of
inlinefunctions (as long as all of them are in different translation units) - So adding
inlinetoPrint(...)will tell the compiler that we know there will be multiple definitions of it and we guarantee that they are all the same!#include <iostream> inline void Print(const std::string& str) { std::cout << str << "\n"; }
- 🚨
inlinecan only be used in function definition - 💡
inlinealso hints to the compiler that it should inline a function --- copy its binary code in-place
Let's change our foo.cpp and bar.cpp a little
foo.cpp
#include <iostream>
inline void Print() {
std::cout << "Foo\n";
}
void Foo() { Print(); }bar.cpp
#include <iostream>
inline void Print() {
std::cout << "Bar\n";
}
void Bar() { Print(); }main.cpp
#include <foo.h>
#include <bar.h>
int main() {
Foo(); Bar(); return 0;
}c++ -std=c++17 -c -I . foo.cpp -o foo.o && ar rcs libfoo.a foo.o
c++ -std=c++17 -c -I . bar.cpp -o bar.o && ar rcs libbar.a bar.o
c++ -std=c++17 main.cpp -L . -I . -lfoo -lbar -o main- We have two functions with the same signature:
void Print();
- The definitions of this function are different and are in different translation units
foo.cppandbar.cpp - When we link them together into
mainthe compiler sees multiple definitions and assumes they are the same - It picks the first one it sees and discard the other one
- Don't use
inlinein source files - ✅ Always use
inlineif you define functions in headers - ✅ Do the same for constants 🔼1️⃣7️⃣
inline constexpr auto kConst = 42;
- ✅ Use namespaces rigorously
- ✅ Use unnamed namespaces in your source files for functions and constants used only within that source file
foo.cpp
#include <iostream>
namespace {
void Print() {
std::cout << "Foo\n";
}
} // namespace
void Foo() { Print(); }bar.cpp
#include <iostream>
namespace {
void Print() {
std::cout << "Bar\n";
}
} // namespace
void Bar() { Print(); }- Use libraries to reuse/share your code
- You have 3 options for libraries:
- Header-only
- Static
- Dynamic
- Each has their own benefits and downsides
- In this course we will mostly use a combination of header-only and static libraries

