Make variables and pattern rules
A Makefile with one rule per file becomes a list of copies as a project grows. Variables and pattern rules replace the copies: one rule compiles any .cpp into a .o, and the link rule names the objects.
This lesson turns the single-file Makefile of lesson 097 into the shape a small project uses, and shows the one dependency make cannot guess: the header that a source includes.
A pattern rule is a template, not a list: make tries it for every target whose name ends in .o and whose stem has a matching source. That is why one rule covers a thousand files and why a typo in the pattern affects all of them.
The variables also make a cross build possible: setting CXX to another compiler on the command line overrides the file without editing it, which is how a build server builds for another target.
A variable can be overridden on the command line, and a target can be built with make app CXXFLAGS=-O2 without editing the file. That is how a build server or a packager changes one thing.
Variables
CXX and CXXFLAGS name the compiler and its flags, and the recipes use $(CXX) and $(CXXFLAGS). The values are expanded when the recipe runs, so one line changes every compile in the file.
The program builds a two-file project: main.cpp calls square from math.cpp. The Makefile compiles each source to an object and links them, and the binary prints 25.
The variable names are a convention, not a rule. CXX and CXXFLAGS are the ones make itself uses for its built-in rules, so a file that sets them also changes the implicit rules.
square.h
#pragma once
int square(int value);
math.cpp
#include "square.h"
int square(int value)
{
return value * value;
}
main.cpp
#include <iostream>
#include "square.h"
int main()
{
std::cout << square(5) << '\n';
}
Makefile
CXX = g++
CXXFLAGS = -std=c++23 -Wall -Wextra
app: main.o math.o
$(CXX) $(CXXFLAGS) main.o math.o -o app
main.o: main.cpp square.h
$(CXX) $(CXXFLAGS) -c main.cpp -o main.o
math.o: math.cpp square.h
$(CXX) $(CXXFLAGS) -c math.cpp -o math.o
clean:
rm -f *.o app
.PHONY: clean
make
./app
g++ -std=c++23 -Wall -Wextra -c main.cpp -o main.o
g++ -std=c++23 -Wall -Wextra -c math.cpp -o math.o
g++ -std=c++23 -Wall -Wextra main.o math.o -o app
25
A pattern rule
%.o: %.cpp is a rule for every object: the stem matches the file name without the extension, so main.o is built from main.cpp and math.o from math.cpp with one recipe.
The recipe uses $< for the first prerequisite and $@ for the target. The program compiles both files through the pattern rule and prints the result of the program.
The link rule still lists the objects by name, because make cannot guess which ones belong together. A variable such as OBJECTS = main.o math.o keeps the list in one place.
square.h
#pragma once
int square(int value);
math.cpp
#include "square.h"
int square(int value)
{
return value * value;
}
main.cpp
#include <iostream>
#include "square.h"
int main()
{
std::cout << square(6) << '\n';
}
Makefile
CXX = g++
CXXFLAGS = -std=c++23 -Wall -Wextra
OBJECTS = main.o math.o
app: $(OBJECTS)
$(CXX) $(CXXFLAGS) $(OBJECTS) -o app
%.o: %.cpp
$(CXX) $(CXXFLAGS) -c $< -o $@
clean:
rm -f *.o app
.PHONY: clean
make
./app
g++ -std=c++23 -Wall -Wextra -c main.cpp -o main.o
g++ -std=c++23 -Wall -Wextra -c math.cpp -o math.o
g++ -std=c++23 -Wall -Wextra main.o math.o -o app
36
The header the pattern rule misses
The pattern rule does not mention square.h, so on its own make has no idea that a change to the header requires a recompile. The build below asks the compiler which headers a source used and includes the answer, so square.h becomes a prerequisite like any other file.
The commands build the project, touch square.h, build again and run. The second make recompiles both objects, because the generated files name square.h, and the transcript shows the two compiles before the link and then the new result.
The fix is to compile with -MMD and include the generated .d files, which list every header a source uses. The -MMD flag writes the files and the -include line reads them, and the next make after a header change recompiles.
square.h
#pragma once
int square(int value);
math.cpp
#include "square.h"
int square(int value)
{
return value * value;
}
main.cpp
#include <iostream>
#include "square.h"
int main()
{
std::cout << square(6) << '\n';
}
Makefile
CXX = g++
CXXFLAGS = -std=c++23 -Wall -Wextra
OBJECTS = main.o math.o
app: $(OBJECTS)
$(CXX) $(CXXFLAGS) $(OBJECTS) -o app
%.o: %.cpp
$(CXX) $(CXXFLAGS) -MMD -c $< -o $@
-include $(OBJECTS:.o=.d)
clean:
rm -f *.o *.d app
.PHONY: clean
make
sleep 1; touch square.h
make
./app
g++ -std=c++23 -Wall -Wextra -MMD -c main.cpp -o main.o
g++ -std=c++23 -Wall -Wextra -MMD -c math.cpp -o math.o
g++ -std=c++23 -Wall -Wextra main.o math.o -o app
g++ -std=c++23 -Wall -Wextra -MMD -c main.cpp -o main.o
g++ -std=c++23 -Wall -Wextra -MMD -c math.cpp -o math.o
g++ -std=c++23 -Wall -Wextra main.o math.o -o app
36
A missing object usually means a missing source, and make reports it before the compiler is started. Reading that message first saves a search through the code for a bug that is only a file name.
A rule that is never used
The Makefile below asks for an object that has no source. The link rule needs helper.o, the pattern rule could build it from helper.cc, and there is no helper.cc in the directory.
The program runs make and gets No rule to make target 'helper.o', needed by 'app'. The message names the missing object and the target that wanted it, and make stops before it runs the compiler.
The same message appears when the pattern is wrong, when a file was renamed, or when the object list and the sources disagree. It is a statement about missing files, not about the code, and the first thing to check is the spelling.
app: helper.o
g++ helper.o -o app
%.o: %.cc
g++ -std=c++23 -c $< -o $@
make
make: *** No rule to make target 'helper.o', needed by 'app'. Stop.
Exercises
- Add a debug target that builds the same objects with -g -O0 into a different executable.
- Move the object list into a variable and add a third source file to it.
- Remove the -include line and check that a header change no longer rebuilds the objects.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does
$(CXXFLAGS)expand to in the lesson's Makefile?Answer
The list of compiler flags
CXXFLAGS holds the flags and is expanded wherever it appears in a recipe.
-
What does the pattern rule
%.o: %.cppmean?Answer
Every .o file is built from a .cpp file with the same stem
The % matches the stem, so one recipe covers every source and its object.
-
What does
$@mean in a recipe?Answer
The target of the rule
$@ is the target, $< is the first prerequisite and $^ is all prerequisites.
-
What does
$<mean in a recipe?Answer
The first prerequisite
It expands to the first prerequisite, which is the source in a compile rule.
-
What does this recipe run?
%.o: %.cppwith\t$(CXX) $(CXXFLAGS) -c $< -o $@and target math.oAnswer
g++ -std=c++23 -Wall -Wextra -c math.cpp -o math.o$< is math.cpp and $@ is math.o, with the flags between them.
-
Why does the pattern rule need
square.has a prerequisite to rebuild after a header change?Answer
Because make compares timestamps and does not read the source
make does not parse #include; the dependency file from -MMD adds the headers it found.
-
What does
-MMDadd to a compile?Answer
A .d file listing the headers the source includes
The compiler writes the makefile-style dependency list next to the object.
-
What does
-include $(OBJECTS:.o=.d)do?Answer
Includes the .d files when they exist and says nothing when they do not
The leading dash makes a missing include silent, which is needed on the first build.
-
What does make print for
appwhen main.cc does not exist and the rule is for %.cc?Answer
No rule to make target 'main.o'The pattern has no source to match, so make cannot build the object and says which target it needed.
-
Why keep the object list in a variable such as OBJECTS?
Answer
So the link rule and the dependency include stay in one place
The list is used by the link rule and by the -include line; a variable keeps the two consistent.
The code on this page was built with g++ 15.2.0 and GNU Make 4.4.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.
Article author: Arthur Isaev