我是 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
, 但它看起来像 CMake
在 target_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 $@
你也可以看看这两个很好的答案:
在您的情况下,更合适的
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/