c++ - 如何避免忘记 make/CMake 中的依赖关系?

标签 c++ makefile cmake

我是 C++ 新手,正在尝试掌握诸如 make/CMake 之类的构建系统的窍门。从 Go 开始,如果你忘记做一件小事,你的二进制文件就会变得陈旧,这似乎是一个持续的风险。特别是,我找不到记住在 make/CMake 中更新依赖项/先决条件的最佳实践。我希望我遗漏了一些明显的东西。

例如,假设我有一个只编译 main.cpp 的基本 makefile :

CFLAGS = -stdlib=libc++ -std=c++17

main: main.o
    clang++ $(CFLAGS) main.o -o main

main.o: main.cpp
    clang++ $(CFLAGS) -c main.cpp -o main.o

主.cpp:
#include <iostream>

int main() {
    std::cout << "Hello, world\n";
}

到现在为止还挺好; make按预期工作。但是假设我有一些其他的仅标题库,名为 cow.cpp :
#include <iostream>

namespace cow {
    void moo() {
        std::cout << "Moo!\n";
    }
}

我决定调用moo()来自main.cpp通过`包括“cow.cpp”:
#include <iostream>
#include "cow.cpp"

int main() {
    std::cout << "Hello, world\n";
    cow::moo();
}

但是,我忘记更新 main.o 的依赖项在 makefile .在运行make的明显测试期间没有发现这个错误。并重新运行二进制 ./main , 因为整个 cow.cpp图书馆直接include d in main.cpp .所以一切看起来都很好,Moo!按预期打印出来。

但是当我改变 cow.cpp打印 Bark!而不是 Moo! ,然后运行 ​​make什么都不做,现在我的./main二进制文件已过期,Moo!仍从 ./main 打印.

我很想知道有经验的 C++ 开发人员如何使用更复杂的代码库来避免这个问题。也许如果你强制自己将每个文件拆分为一个头文件和一个实现文件,你至少能够快速纠正所有这些错误?这似乎也不是万无一失的。因为头文件有时包含一些内联实现。

我的示例使用 make而不是 CMake , 但它看起来像 CMaketarget_link_libraries 中存在相同的依赖项列表问题(虽然传递性有点帮助)。

作为一个相关的问题:似乎显而易见的解决方案是构建系统只查看源文件并推断依赖关系(它可以只进入一层并依赖 CMake 来处理传递性)。有没有理由这不起作用?是否有实际执行此操作的构建系统,或者我应该自己编写?

谢谢!

最佳答案

首先,您需要在 Makefile 中引用依赖文件。 .

这可以通过函数来​​完成

SOURCES := $(wildcard *.cpp)
DEPENDS := $(patsubst %.cpp,%.d,$(SOURCES))

这将采用所有 *.cpp 的名称文件并替换并附加扩展名 *.d命名您的依赖项。

然后在你的代码中
-include $(DEPENDS)
-告诉 Makefile如果文件不存在,不要提示。如果它们存在,它们将被包含并根据依赖关系正确重新编译您的源代码。

最后,可以使用以下选项自动创建依赖项:-MMD -MP用于创建对象文件的规则。 Here你可以找到完整的解释。生成依赖项的是 MMD ; MP是为了避免一些错误。如果您想在系统库更新时重新编译,请使用 MD而不是 MMD .

在您的情况下,您可以尝试:
main.o: main.cpp
    clang++ $(CFLAGS) -MMD -MP -c main.cpp -o main.o

如果您有更多文件,最好使用单一规则来创建目标文件。就像是:
%.o: %.cpp Makefile 
    clang++ $(CFLAGS) -MMD -MP -c $< -o $@

你也可以看看这两个很好的答案:
  • one
  • two

  • 在您的情况下,更合适的 Makefile应该如下所示(可能有一些错误,但请告诉我):
    CXX = clang++
    CXXFLAGS = -stdlib=libc++ -std=c++17
    WARNING := -Wall -Wextra
    
    PROJDIR   := .
    SOURCEDIR := $(PROJDIR)/
    SOURCES   := $(wildcard $(SOURCEDIR)/*.cpp)
    OBJDIR    := $(PROJDIR)/
    
    OBJECTS := $(patsubst $(SOURCEDIR)/%.cpp,$(OBJDIR)/%.o,$(SOURCES))
    DEPENDS := $(patsubst $(SOURCEDIR)/%.cpp,$(OBJDIR)/%.d,$(SOURCES))
    
    # .PHONY means these rules get executed even if
    # files of those names exist.
    .PHONY: all clean
    
    all: main
    
    clean:
        $(RM) $(OBJECTS) $(DEPENDS) main
    
    # Linking the executable from the object files
    main: $(OBJECTS)
        $(CXX) $(WARNING) $(CXXFLAGS) $^ -o $@
    
    #include your dependencies
    -include $(DEPENDS)
    
    #create OBJDIR if not existin (you should not need this)
    $(OBJDIR):
        mkdir -p $(OBJDIR)
    
    $(OBJDIR)/%.o: $(SOURCEDIR)/%.cpp Makefile | $(OBJDIR)
        $(CXX) $(WARNING) $(CXXFLAGS) -MMD -MP -c $< -o $@
    

    编辑回答评论
    作为另一个问题,将 DEPENDS 定义重写为 DEPENDS := $(wildcard $(OBJDIR)/*.d) 是否有任何问题? ?

    好问题,我花了一段时间才明白你的意思

    来自 here

    $(wildcard pattern…) This string, used anywhere in a makefile, is replaced by a space-separated list of names of existing files that match one of the given file name patterns. If no existing file name matches a pattern, then that pattern is omitted from the output of the wildcard function.



    所以wildcard返回与模式匹配的文件名列表。 patsubst作用于字符串,它不关心那些字符串是什么:它被用作创建依赖项的文件名的一种方式,而不是文件本身。在 Makefile我发布的示例 DEPENDS实际上在两种情况下使用:使用 make clean 进行清洁时和 include所以在这种情况下它们都可以工作,因为你没有使用 DEPENDS在任何规则中。存在一些差异(我尝试运行,您也应该确认)。与 DEPENDS := $(patsubst $(SOURCEDIR)/%.cpp,$(OBJDIR)/%.d,$(SOURCES))如果你运行 make clean依赖 *.d没有通讯员*.cpp文件不会被删除,而他们会随着您的更改而删除。相反,您可能包含与您的 *.cpp 无关的依赖项。文件。

    我问 this questions : 来看看答案。

    如果 .d文件因粗心而被删除,但 .o文件仍然存在,那么我们就有麻烦了。在原始示例中,如果 main.d被删除,然后 cow.cpp随后更改,make不会意识到它需要重新编译main.o因此它永远不会重新创建依赖文件。有没有办法廉价地创建 .d文件而不重新编译目标文件?如果是这样,那么我们可能会重新创建所有 /.d每个 make 命令上的文件?

    又是一个好问题。

    是的,你是对的。其实这是我的一个错误。发生这种情况是因为规则
    main: main.o
        $(CXX) $(WARNING) $(CFLAGS) main.o -o main 
    

    实际上应该是:
    main: $(OBJECTS)
        $(CXX) $(WARNING) $(CXXFLAGS) $^ -o $@
    

    以便在其中一个对象更改时重新链接(更新可执行文件),并且只要它们的 cpp 之一更改,它们就会更改文件更改。

    一个问题仍然存在:如果您删除了依赖项而不是对象,并且只更改了一个或多个头文件(而不是源文件),那么您的程序不会更新。

    我还更正了答案的前一部分。

    编辑 2
    要创建依赖关系,您还可以将新规则添加到 Makefile。 :
    here是一个例子。

    关于c++ - 如何避免忘记 make/CMake 中的依赖关系?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54162861/

    相关文章:

    c++ - 函数指针强制转换

    c++ - 枚举和类 - 运行时错误!!

    c++ - 如何防止派生类公开私有(private)/ protected 虚函数?

    sqlite - 如何为 WinRT/ARM 编译 sqlite?

    c++ - 在我的项目中包含 C++ 库的最佳实践

    C++ - 在模板类之外但在头文件中定义成员函数

    c++ - libstdc++ : DSO missing from command line

    c - Visual Studio C 编译器是否具有与 GCC 的 -M 等效的功能?

    c++ - CMake 没有将共享库的链接依赖项传播到我的可执行文件

    c++ - 使用 CMake 构建模块化 C++ 项目