CMake编译静态库时render::ViewRenderer无符号的问题
我是CMake和复杂C++项目新手,尝试将多个类编译为静态库时,发现render::ViewRenderer类(声明于view.hpp,实现于view.cpp)在生成的librender.a中无对应符号。用clang-15编译,切换编译器后问题仍存在。render::WindowRenderer类编译正常,单独将ViewRenderer编译为库、直接编译到可执行文件均无效,仅将实现放入头文件可解决但不符合需求。链接时出现arm64架构下未定义符号错误,相关代码及构建日志如下:
相关代码
view.hpp
#ifndef VIEW_HPP #define VIEW_HPP #include <string> namespace render { enum View { main_menu, game }; /** * A class handling the rendering of a window's contents. */ template <class View> class ViewRenderer { const View view; public: void render(); ViewRenderer(View view); ~ViewRenderer(); }; } #endif
view.cpp
#include "render/view.hpp" template <class View> void render::ViewRenderer<View>::render() { } template <class View> render::ViewRenderer<View>::ViewRenderer(View view) : view{ view } { } template <class View> render::ViewRenderer<View>::~ViewRenderer() { }
window.hpp
#ifndef WINDOW_HPP #define WINDOW_HPP #include <GLFW/glfw3.h> #include <string> namespace render { /** * A class to manage the application's windows. A window opens on init and blocks the thread with its execute method. * @param name the title of the window to open * @param game_renderer the initial GameRenderer to handle the window's contents or nullptr for default behaviour */ enum WindowMode { windowed, fullscreen, windowed_fullscreen }; class WindowRenderer { const char* window_name; GLFWwindow* window; render::WindowMode window_mode; void execute(); public: void (*set_fullscreen)(WindowMode); void (*end)(); WindowRenderer(std::string); ~WindowRenderer(); }; } #endif
window.cpp
#include <GL/glew.h> #include <GLFW/glfw3.h> #include <string> #include <stdexcept> #include "render/window.hpp" render::WindowRenderer::WindowRenderer(std::string _window_name) : window_name{ _window_name.c_str() }, window_mode{ render::fullscreen } { bool glewExperimental = true; if (!glfwInit()) { throw std::runtime_error("Failed to initialize GLFW\n"); return; } GLFWmonitor* monitor = glfwGetPrimaryMonitor(); this->window = glfwCreateWindow(640, 480, this->window_name, NULL, NULL); glfwMakeContextCurrent(window); if (glewInit() != GLEW_OK) { throw std::runtime_error("Failed to initialize GLEW\n"); return; } this->execute(); } void render::WindowRenderer::execute() { do { glClear(GL_COLOR_BUFFER_BIT); glfwSwapBuffers(window); glfwPollEvents(); } while (glfwGetKey(window, GLFW_KEY_ESCAPE) != GLFW_PRESS && glfwWindowShouldClose(window) == 0); } render::WindowRenderer::~WindowRenderer() { }
CMakeLists.txt
set(CMAKE_C_COMPILER /opt/local/bin/clang-mp-15) set(CMAKE_CXX_COMPILER /opt/local/bin/clang++-mp-15) cmake_policy(SET CMP0048 NEW) cmake_minimum_required(VERSION 3.20) if(${CMAKE_SYSTEM_NAME} MATCHES "Linux") set(OPENGL_LIBRARY ${OPENGL_LIBRARY} -lGL -lGLU -lXrandr -lXext -lX11 -lrt ${CMAKE_DL_LIBS} ${GLFW_LIBRARIES} ) elseif(${CMAKE_SYSTEM_NAME} MATCHES "Darwin") set(OPENGL_LIBRARY ${OPENGL_LIBRARY} ${CMAKE_DL_LIBS} ${GLFW_LIBRARIES} ) endif(${CMAKE_SYSTEM_NAME} MATCHES "Linux") add_definitions( -DTW_STATIC -DTW_NO_LIB_PRAGMA -DTW_NO_DIRECT3D -DGLEW_STATIC -D_CRT_SECURE_NO_WARNINGS ) project( game VERSION 0.0.1 LANGUAGES CXX C OBJC OBJCXX ) include(CheckCXXCompilerFlag) CHECK_CXX_COMPILER_FLAG("-std=c++20" COMPILER_SUPPORTS_CXX20) if(COMPILER_SUPPORTS_CXX20) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++20") else() message(STATUS "The compiler ${CMAKE_CXX_COMPILER} has no C++20 support. Please use a different C++ compiler.") endif() # ---- Libraries ---- set(SOURCE ${CMAKE_SOURCE_DIR}/src/app/render/window.cpp ${CMAKE_SOURCE_DIR}/src/app/include/render/window.hpp ${CMAKE_SOURCE_DIR}/src/app/render/view.cpp ${CMAKE_SOURCE_DIR}/src/app/include/render/view.hpp ) # ---- External libraries ---- set(GLEW_VERSION 2.1.0) set (GLFW_VERSION 3.3.8) set(GLM_VERSION 0.9.9.8) # >>>>>>>>>>>>>>>>>>>>>>>> GLEW add_subdirectory(${CMAKE_SOURCE_DIR}/lib/glew-${GLEW_VERSION}/build/cmake) #this produces glew target set(GLEW_INCLUDE ${CMAKE_SOURCE_DIR}/lib/glew-${GLEW_VERSION}/include) # >>>>>>>>>>>>>>>>>>>>>>>> GLFW add_subdirectory(lib/glfw-${GLFW_VERSION}) SET(GLFW_INCLUDE lib/glfw-${GLFW_VERSION}/include) # >>>>>>>>>>>>>>>>>>>>>>>> GLM SET(GLM_INCLUDE lib/glm-${GLM_VERSION}) # ----------------------------- include_directories( ${CMAKE_SOURCE_DIR}/src/app/include ${GLEW_INCLUDE} ${GLFW_INCLUDE} ${GLM_INCLUDE} ) add_executable( game_exe ${CMAKE_SOURCE_DIR}/src/app/main.cpp ) set(TARGET game_exe PROPERTY CMAKE_CXX_STANDARD 20) add_library( render ${CMAKE_SOURCE_DIR}/src/app/render/window.cpp ${CMAKE_SOURCE_DIR}/src/app/include/render/window.hpp ${CMAKE_SOURCE_DIR}/src/app/render/view.cpp ${CMAKE_SOURCE_DIR}/src/app/include/render/view.hpp ) target_link_libraries( game_exe render glfw glew )
项目结构
app ... src include render view.hpp window.hpp render view.cpp window.cpp main.cpp ... CMakeLists.txt
构建错误信息
[main] Building folder: game [build] Starting build [proc] Executing command: /opt/homebrew/bin/cmake --build /Users/matt/projects/game/build --config Debug --target all -- [build] [3/5 20% :: 0.445] Building CXX object CMakeFiles/render.dir/src/app/render/view.cpp.o [build] [3/5 40% :: 0.447] Building CXX object CMakeFiles/render.dir/src/app/render/window.cpp.o [build] [4/5 60% :: 0.448] Building CXX object CMakeFiles/game_exe.dir/src/app/main.cpp.o [build] [4/5 80% :: 0.474] Linking CXX static library librender.a [build] [5/5 100% :: 0.619] Linking CXX executable game_exe [build] FAILED: game_exe [build] : && /opt/local/bin/clang++-mp-15 -std=c++20 -arch arm64 -isysroot /Library/Developer/CommandLineTools/SDKs/MacOSX13.0.sdk -Wl,-search_paths_first -Wl,-headerpad_max_install_names CMakeFiles/game_exe.dir/src/app/main.cpp.o -o game_exe -Wl,-rpath,/Users/matt/projects/game/build/lib librender.a lib/glfw-3.3.8/src/libglfw3.a lib/libGLEW.2.1.0.dylib -framework Cocoa -framework IOKit -framework CoreFoundation -Xlinker -framework -Xlinker OpenGL && : [build] Undefined symbols for architecture arm64: [build] "render::ViewRenderer<render::View>::ViewRenderer(render::View)", referenced from: [build] _main in main.cpp.o [build] "render::ViewRenderer<render::View>::~ViewRenderer()", referenced from: [build] _main in main.cpp.o [build] ld: symbol(s) not found for architecture arm64 [build] clang: error: linker command failed with exit code 1 (use -v to see invocation) [build] ninja: build stopped: subcommand failed. [proc] The command: /opt/homebrew/bin/cmake --build /Users/matt/projects/game/build --config Debug --target all -- exited with code: 1 and signal: null [build] Build finished with exit code 1 [cpptools] The build configurations generated do not contain the active build configuration. Using "" for CMAKE_BUILD_TYPE instead of "Debug" to ensure that IntelliSense configurations can be found
问题原因及解决方案
问题根源
C++模板是代码生成机制,编译器只有在看到模板的具体实例化代码时,才会生成对应参数的机器码。当你把ViewRenderer的实现放在view.cpp中时,编译这个文件的阶段,编译器不知道你后续会用render::View作为模板参数来实例化类,所以不会生成ViewRenderer<render::View>的构造、析构及render方法的代码,导致静态库librender.a中没有这些符号,链接可执行文件时就会报错。
可行解决方案
1. 显式实例化模板(推荐)
在view.cpp的末尾添加显式实例化代码,告诉编译器生成特定模板参数的类实例:
// 在view.cpp最后添加 template class render::ViewRenderer<render::View>;
这样编译view.cpp时,编译器就会生成ViewRenderer<render::View>的所有成员函数代码,静态库中就会包含对应的符号,链接时就能找到。
2. 将模板实现移至头文件
这是C++模板最通用的写法,把成员函数的实现直接放在头文件的类定义中,或者头文件的末尾。比如修改view.hpp:
// view.hpp中修改类定义 template <class View> class ViewRenderer { const View view; public: void render() { // 实现代码 } ViewRenderer(View view) : view{ view } { // 实现代码 } ~ViewRenderer() { // 实现代码 } };
这样任何包含该头文件的代码在实例化模板时,编译器都能获取到实现并生成对应代码。
3. 包含实现文件的技巧
在view.hpp的末尾添加:
// view.hpp最后一行 #include "../render/view.cpp"
同时修改view.cpp,添加保护宏防止重复编译:
#ifndef VIEW_CPP_IMPL #define VIEW_CPP_IMPL // 原view.cpp的所有代码 #endif // VIEW_CPP_IMPL
这样包含头文件的代码会自动引入实现,编译器能生成实例代码,同时单独编译view.cpp时也不会出现重复定义问题。
内容的提问来源于stack exchange,提问作者MattC

