1. 前言

掌握一门编程语言的关键之一就是多动手写代码。 当我不知道该写什么代码时,通常会选择造轮子。 造轮子的过程中不仅能让我更加熟悉语言的特性,还能让我深入思考这些轮子的底层实现原理。 同时,造好的轮子也可以用于以后的项目中,从而提高开发效率。

C++ 是一门非常适合造轮子的语言,搭配上 CMake 这个构建工具,可以轻松构建出一个专属于自己的轮子库。 这样在以后的项目中,只需要引入这个库,就可以使用自己造的轮子了。

例如,当我曾经实现了一个线程池,未来如果要使用它, 只需要 include 我的 wheel 库:

#include <wheel/thread_pool.hpp>

int main() {
    wheel::ThreadPool thread_pool(4);

    ...
}

本文将会用一个简单的例子来介绍如何使用 C++ 和 CMake 来构建一个专属于自己的轮子库。

2. 例子介绍

本文的目标是实现一个加法器,输入两个整数,加法器会输出它们的和。 这个加法器用到了wheel库中的math.hpp文件里面的add函数。

3. 构建轮子库

首先来看一下最终的 wheel 库目录结构:

wheel
├── CMakeLists.txt
├── include
│   └── wheel
│       └── math.hpp
└── src
    └── math.cpp

约定include/wheel内存的头文件是对外公开的接口,src内存源文件(也可以存私有头文件),test内存的测试文件。

接下来我们分别来看一下这几个文件的内容。

include/wheel/math.hpp定义要暴露的接口:

#pragma once

namespace wheel {

int add(int a, int b);

}  // namespace wheel

信息

任何 C++ 项目都要有单独的命名空间,以避免与其他库命名冲突。

src/math.cpp实现接口的功能:

#include <wheel/math.hpp>

namespace wheel {

int add(int a, int b) {
    return a + b;
}

}  // namespace wheel

CMakelists.txt文件内容如下:

cmake_minimum_required(VERSION 3.16)

option(BUILD_WHEEL_TEST "build wheel test" OFF)

set(TARGET wheel)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_EXPORT_COMPILE_COMMANDS 1)

project(
    ${TARGET}
    VERSION 0.1.0
    DESCRIPTION "wheel"
    HOMEPAGE_URL "https://github.com/m1dsolo/wheel"
    LANGUAGES C CXX
)

file(GLOB_RECURSE SRC CONFIGURE_DEPENDS "src/*.cpp")
add_library(${TARGET} OBJECT ${SRC})
target_include_directories(${TARGET} PUBLIC include)

if (BUILD_WHEEL_TEST)
    add_subdirectory(test)
endif()

如果有看不懂的选项,可以让 GPT 帮你讲解~

接下来使用以下命令来进行构建和编译:

cmake -B build -G Ninja
cmake --build build -j8

4. 使用轮子库

创建一个新项目adder,其目录结构如下:

adder
├── CMakeLists.txt
├── src
│   └── main.cpp
└── third_party
    └── wheel

src/main.cpp内容如下:

#include <iostream>

#include <wheel/math.hpp>

int main() {
    int res = wheel::add(1, 2);
    std::cout << res << std::endl;

    return 0;
}

CMakeLists.txt内容如下:

cmake_minimum_required(VERSION 3.16)

set(TARGET adder)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_EXPORT_COMPILE_COMMANDS 1)

project(
    ${TARGET}
    VERSION 0.1.0
    DESCRIPTION "adder"
    HOMEPAGE_URL "https://github.com/m1dsolo/adder"
    LANGUAGES C CXX
)

set(WHEEL_ROOT ${PROJECT_SOURCE_DIR}/third_party/wheel)

add_subdirectory(${WHEEL_ROOT})

file(GLOB_RECURSE SRC CONFIGURE_DEPENDS "src/*.cpp")
add_executable(${TARGET} ${SRC})
target_include_directories(${TARGET}
    PRIVATE include
    PRIVATE ${WHEEL_ROOT}/include
)
target_link_libraries(${TARGET}
    PRIVATE wheel
)

也就是编译时同时需要编译并链接wheel库。

接下来构建并编译adder程序:

cmake -B build -G Ninja
cmake --build build -j8

编译后我们就可以成功运行adder了:

./build/adder

可以看到终端上输出了我们的预期结果。

5. 测试轮子库(可选)

轮子库本质上就是一个 C++项目,推荐在写好轮子后,写一些测试用例来验证轮子的正确性。 这里我们选择使用 GoogleTest 来进行测试。

wheel内添加:

wheel
├── test
│   ├── CMakeLists.txt
│   └── math.cpp
└── third_party

安装 GoogleTest 到third_party/googletest

git submodule add --depth=1 https://github.com/google/googletest.git third_party/googletest

test/CMakeLists.txt中添加:

set(TARGET test)
set(GTEST_ROOT ${PROJECT_SOURCE_DIR}/third_party/googletest)

add_subdirectory(${GTEST_ROOT} ${CMAKE_BINARY_DIR}/googletest)

enable_testing()

file(GLOB_RECURSE SRC "*.cpp")
add_executable(${TARGET} ${SRC})
target_include_directories(${TARGET}
    PRIVATE ${GTEST_ROOT}/include
    PRIVATE ${PROJECT_SOURCE_DIR}/include
)
target_link_libraries(${TARGET}
    PRIVATE GTest::gtest_main
    PRIVATE wheel
)

add_test(NAME ${TARGET} COMMAND ${TARGET})

test/math.cpp添加对 math 的测试用例:

#include <wheel/math.hpp>

#include <gtest/gtest.h>

namespace wheel {

TEST(MathTest, AddFunction) {
    EXPECT_EQ(wheel::add(1, 1), 2);
    EXPECT_EQ(wheel::add(-1, 1), 0);
    EXPECT_EQ(wheel::add(-1, -1), -2);
    EXPECT_EQ(wheel::add(0, 0), 0);
}

}  // namespace wheel

由于之前我们已经配置了BUILD_WHEEL_TEST这个 option ,所以只需要在构建项目的时候指定这个参数:

cmake -B build -G Ninja -DBUILD_WHEEL_TEST=ON
cmake --build build -j8

编译成功后运行测试:

./build/test/test

6. 结语

在完成轮子库的构建后,建议撰写一些笔记或博客,记录下整个过程和思考。 这不仅有助于巩固所学知识,还能在未来需要时快速回顾和使用这些轮子库。 通过不断地实践和总结,能够更深入地理解编程语言和工具的特性,从而提升开发效率和代码质量。

你可以参考我分享的 轮子库 , 代码仅供学习参考,不建议直接用于生产环境。

以后有时间,我会分享一些曾经造过的轮子的实现原理,敬请期待!