Why this matters
A checkerboard is a grid of squares that alternate between two colours. A grid is any picture made of rows and columns of cells, such as a tic-tac-toe board, a seating plan or a table with lines drawn between the cells. A box is a grid where the outer cells form a border and the inner cells are filled.
Each of these pictures is made from the same two loops you already know. The outer loop goes through the rows. The inner loop goes through the cells in one row. The new part is the rule that decides what to print in each cell. For a checkerboard the rule depends on whether the row number plus the column number is even or odd. For a box the rule depends on whether the cell touches the edge.
The earlier lessons in this module built shapes from row widths and centring. This lesson builds shapes from the position of each cell, so the same rule can draw any size of grid.
This lesson uses C++ with the standard output stream std::cout, and std::string, a type that holds text, for building a picture before printing it. Compile the samples with g++ -std=c++20 -Wall. The output shown under each sample was produced by running the sample on g++ 15.2.
A grid is a loop inside a loop
Start with the plain grid. Suppose you want three rows, each with three cells, and each cell printed as #. The outer loop counts rows from 0 to 2. The inner loop counts columns from 0 to 2. After the inner loop finishes, you print a newline so the next row starts on a new line.
#include <iostream>
int main() {
for (int row = 0; row < 3; ++row) {
for (int col = 0; col < 3; ++col) {
std::cout << '#';
}
std::cout << '\n';
}
}Output:
###
###
###Counting starts at 0 in C++, so the first row is row 0 and the first column is column 0. The last valid index is one less than the size. That is why the loop condition is row < 3, not row <= 3. The next sections use this counting rule many times.
Each cell has a pair of numbers: its row and its column. The pair (1, 2) means row 1, column 2. Every rule in this lesson looks at that pair and decides what to print.
Even and odd, with the remainder operator
A number is even if it can be split into two equal whole parts. 4 is even. 5 is odd. In C++, the operator % gives the remainder after division. The expression 4 % 2 is 0, because 4 divides by 2 with nothing left over. The expression 5 % 2 is 1, because 2 goes into 5 twice with 1 left over.
So a test x % 2 == 0 means that x is even, and x % 2 == 1 means that x is odd. The test uses ==, the equal test, which compares two values. A single = would store a value in a variable instead, and that is a different job.
The checkerboard rule uses the sum of the row and the column. Look at a 3 by 3 grid where each cell shows its row plus column:
0 1 2
1 2 3
2 3 4Moving one cell to the right adds 1 to the sum. Moving one cell down also adds 1. Each step flips the sum from even to odd or from odd to even. So the sums alternate, and the cells whose sum is even form a checkerboard pattern. The cell at the top left has sum 0, which is even. Its neighbours to the right and below have sum 1, which is odd.
The rule is written as:
(row + col) % 2 == 0The brackets are needed. The operator % binds more tightly than +, so row + col % 2 would compute col % 2 first. The next section shows what that mistake does.
Your first checkerboard
Here is a complete program for a 3 by 3 checkerboard. Even cells print #, odd cells print ..
#include <iostream>
int main() {
const int rows = 3;
const int cols = 3;
for (int row = 0; row < rows; ++row) {
for (int col = 0; col < cols; ++col) {
if ((row + col) % 2 == 0) {
std::cout << '#';
} else {
std::cout << '.';
}
}
std::cout << '\n';
}
}Output:
#.#
.#.
#.#Read the program in order. The two const int lines hold the size. const means the value cannot be changed after it is set. The outer loop prints one row at a time. The inner loop prints one cell at a time. The if picks the character. The else branch handles every odd sum. After each row, std::cout << '\n' moves to the next line.
If you want a 5 by 7 board, change the two numbers and nothing else. The loops and the rule stay the same. This is the main reason to use a rule instead of typing the pattern by hand.
Look at the first row. Cell (0, 0) has sum 0, so it prints #. Cell (0, 1) has sum 1, so it prints .. Cell (0, 2) has sum 2, so it prints #. The output row is #.#. The second row starts with . because cell (1, 0) has sum 1, which is odd.
Choosing the two characters in a function
A program that prints directly is fine for one picture. A better habit is to put the drawing in a function that returns the picture as text. The caller can then print it, save it or test it, without the function needing to know what happens next.
The function below takes the size and the two characters to use. Each one has the type char, which holds a single character. It builds a std::string by adding one character at a time with +=. The text += means "add this to the end of what is already there". At the end of each row it adds a newline.
#include <iostream>
#include <string>
std::string checkerboard(int rows, int cols, char dark, char light) {
std::string picture;
for (int row = 0; row < rows; ++row) {
for (int col = 0; col < cols; ++col) {
picture += ((row + col) % 2 == 0) ? dark : light;
}
picture += '\n';
}
return picture;
}
int main() {
std::cout << checkerboard(4, 8, '#', ' ');
std::cout << '\n';
std::cout << checkerboard(3, 5, 'X', 'O');
}Output (a blank line separates the two boards):
# # # #
# # # #
# # # #
# # # #
XOXOX
OXOXO
XOXOXThe part ((row + col) % 2 == 0) ? dark : light is a conditional expression. A question mark and a colon choose one of two values. If the test before the question mark is true, the value before the colon is used. Otherwise the value after the colon is used. It does the same job as the if and else in the previous program, but it fits on one line and produces one value.
The first board has a space as the light cell, so the rows that start with # end with a trailing space. A trailing space is a space at the end of a line. It is invisible, but it is still a character in the output. This matters when you compare output in a test.
The function still does not decide what the characters are. The caller decides that. This makes the function useful for a board of letters, a board of symbols or a board of coloured blocks, as long as the caller passes one character for each colour.
Drawing a box with a border
A box has a border and a fill. The border is the outer ring of cells. The fill is everything inside. To test whether a cell is on the border, look at its position:
- the row is 0 (the top row) or the row is the last row,
height - 1 - the column is 0 (the left column) or the column is the last column,
width - 1
If any of these four tests is true, the cell is on the edge. Each test gives a bool, a type that holds either true or false. The || operator means "or". It is true if at least one side is true. The last index is height - 1 and not height because counting starts at 0. A box that is 4 rows tall has rows 0, 1, 2 and 3. Row 3 is height - 1.
#include <iostream>
void draw_box(int width, int height, char border, char fill) {
for (int row = 0; row < height; ++row) {
for (int col = 0; col < width; ++col) {
bool on_edge = row == 0 || row == height - 1 || col == 0 || col == width - 1;
std::cout << (on_edge ? border : fill);
}
std::cout << '\n';
}
}
int main() {
draw_box(7, 4, '#', '.');
std::cout << '\n';
draw_box(5, 3, '*', ' ');
}Output:
#######
#.....#
#.....#
#######
*****
* *
*****The variable on_edge has the type bool. Giving the test a name makes the next line easier to read. The line std::cout << (on_edge ? border : fill); uses the same conditional expression as before. The brackets around the conditional are needed here, because << would otherwise be read before the question mark.
The second box uses a space as its fill. The result is an empty box, which is often called a hollow box. The fill can be any character, including a space.
A box with a checkerboard inside
Now combine the two ideas. The border is solid. The inside is a checkerboard. The easiest way to build this is to make one function that answers one question: what goes in cell (row, col)? The picture function then only has to loop over every cell and ask that question.
The order of the tests matters. The border test comes first, so a border cell is always solid, even when its sum would have made it part of the pattern.
#include <iostream>
#include <string>
// Returns one character for the cell at (row, col) in a box of the given size.
// The border wins over the pattern, so the edge is always solid.
char cell_at(int row, int col, int width, int height) {
bool on_edge = row == 0 || row == height - 1 || col == 0 || col == width - 1;
if (on_edge) {
return '#';
}
return (row + col) % 2 == 0 ? 'X' : ' ';
}
std::string boxed_checker(int width, int height) {
std::string picture;
for (int row = 0; row < height; ++row) {
for (int col = 0; col < width; ++col) {
picture += cell_at(row, col, width, height);
}
picture += '\n';
}
return picture;
}
int main() {
std::cout << boxed_checker(9, 5);
}Output:
#########
#X X X X#
# X X X #
#X X X X#
#########The cell_at function returns a single char. Its job is small: given a position, return a character. The return statement ends the function and gives its value back to the caller.
The inside pattern uses the box's own row and column numbers, not numbers counted from the inside corner. Cell (1, 1) is the first inside cell, and its sum is 2, which is even, so it prints X. Cell (1, 2) has sum 3, so it prints a space. This is why the first inside row starts with X after the border.
If you wanted the inside pattern to start from the first inside cell, you would subtract 1 from the row and column before testing them. That is a choice. The lesson uses the box's own coordinates because they are simpler to explain.
Grid art with lines between cells
The last example is the hardest. It draws a grid with lines between cells, like the table below:
+---+---+
| | |
+---+---+
| | |
+---+---+Think about this picture in characters, not in cells. Each cell is 3 characters wide inside its walls, and each wall takes one more character. So one cell takes 4 characters across. The picture also has one more wall line than there are cells, so a grid with 2 cells across has 2 times 4 plus 1 columns, which is 9.
Vertically, each cell takes two lines in the picture. One line holds the cell's content, and the next line is the wall below it. A grid with 2 cells down has 2 times 2 plus 1 lines, which is 5.
Now the rule is clear. For a picture line r and a picture column c:
- if
ris even, the line is a wall line. It prints+at columns that are multiples of 4, and-everywhere else - if
ris odd, the line is a cell line. It prints|at columns that are multiples of 4, and a space everywhere else
#include <iostream>
#include <string>
// Draws a grid of empty cells. Each cell is 4 characters wide and 2 lines tall,
// plus one extra line and one extra column for the outer edge.
std::string grid(int cells_across, int cells_down) {
int out_rows = 2 * cells_down + 1;
int out_cols = 4 * cells_across + 1;
std::string picture;
for (int r = 0; r < out_rows; ++r) {
for (int c = 0; c < out_cols; ++c) {
bool on_line_row = r % 2 == 0;
bool on_line_col = c % 4 == 0;
if (on_line_row) {
picture += on_line_col ? '+' : '-';
} else {
picture += on_line_col ? '|' : ' ';
}
}
picture += '\n';
}
return picture;
}
int main() {
std::cout << grid(2, 2);
std::cout << '\n';
std::cout << grid(3, 1);
}Output:
+---+---+
| | |
+---+---+
| | |
+---+---+
+---+---+---+
| | | |
+---+---+---+The two tests r % 2 == 0 and c % 4 == 0 are the same kind of test as the checkerboard rule. The first asks whether a number is a multiple of 2, and the second asks whether a number is a multiple of 4. The rest of the function is loops and string building that you have already seen.
The variables out_rows and out_cols give the size of the picture in characters. Naming them makes the loop bounds clear. A reader can see that the picture has one more line than twice the number of cells down.
The function has no picture-specific code. Change the two arguments and the grid changes size. The same test works for 1 cell or 100 cells.
Common mistakes
Missing brackets around the sum
A common mistake is to write the checkerboard test without the brackets around row + col:
if (row + col % 2 == 0) {This compiles without any message, because the code is valid C++. The mistake only shows in the picture. The operator % runs before +, so the test adds row to the remainder of col divided by 2. Here is the output of the program in the first checkerboard section with that one line changed:
#.#
...
...Only the first row looks like a checkerboard, and the rest is wrong. When a pattern looks wrong, print a small table of the values the test sees. A 3 by 3 table of row + col % 2 would have shown the problem at once.
Using <= where < is needed
Counting starts at 0, so the last valid row is height - 1. A loop written with <= runs one time too many:
for (int row = 0; row <= height; ++row) {Here is the output of a box drawn with <= in both the row and the column loop, for a width of 5 and a height of 3:
######
#...#.
######
#...#.The picture has four lines, not three, and six columns, not five. The real bottom border is on line 3, and line 4 is an extra row that the loop draws as if it were inside the box. The extra column on the right is filled with . for the same reason. Use < in every loop that counts over a size, and use size - 1 only when you need the index of the last cell.
Comparing an int with a size
The standard library's std::vector reports its size as an unsigned type. An int is signed, which means it can be negative. Comparing the two can give a warning. Here is the message from a loop written with int and cells.size():
m3_sign.cpp: In function ‘int main()’:
m3_sign.cpp:6:23: warning: comparison of integer expressions of different signedness: ‘int’ and ‘std::vector<int>::size_type’ {aka ‘long unsigned int’} [-Wsign-compare]
6 | for (int i = 0; i < cells.size(); ++i) {
| ~~^~~~~~~~~~~~~~The program still builds and runs, but the warning says the comparison may not do what you expect. The fix used in this lesson is std::ssize, which returns the size as a signed value. It was added in C++20:
for (int i = 0; i < std::ssize(cells); ++i) {The program compiles with no warning, and it prints 1, 2 and 3, the same as the loop with the warning.
Forgetting the semicolon
A missing semicolon at the end of a statement is one of the most common mistakes. The compiler often reports the error on the line after the one that is wrong. Here is a program where int width = 4 has no semicolon:
#include <iostream>
int main() {
int width = 4
for (int col = 0; col < width; ++col) {
std::cout << '#';
}
std::cout << '\n';
}The compiler reports:
m4_semicolon.cpp: In function ‘int main()’:
m4_semicolon.cpp:5:5: error: expected ‘,’ or ‘;’ before ‘for’
5 | for (int col = 0; col < width; ++col) {
| ^~~
m4_semicolon.cpp:5:23: error: ‘col’ was not declared in this scope
5 | for (int col = 0; col < width; ++col) {
| ^~~The first error points at the for on line 5, but the real problem is the missing semicolon at the end of line 4. The second error is a side effect of the first. When a message points at a line that looks correct, check the line above it.
Forgetting the newline
If the picture loop does not add '\n' at the end of each row, every row runs into the next. A 3 by 3 checkerboard then prints as one line of nine characters. No error appears. The only sign is the picture itself, so check the output shape after every change.
Practice
Exercise 1: Predict the output
Without running the code, predict the output of this call and then check it:
std::cout << checkerboard(2, 4, 'A', 'B');Use the checkerboard function from the section on choosing two characters.
Show the answer
The output is two lines:
ABAB
BABAThe cell (0, 0) has sum 0, so it is A. The cell (1, 0) has sum 1, so it is B. Each cell alternates from its neighbours, so the second row starts with B and alternates from there.
Exercise 2: Border and checker fill
Write a function ex2 that draws a box with a # border. The inside alternates between + and a space, using the same (row + col) % 2 rule. Draw a box that is 6 wide and 3 tall.
Show the answer
std::string ex2(int width, int height) {
std::string picture;
for (int row = 0; row < height; ++row) {
for (int col = 0; col < width; ++col) {
bool on_edge = row == 0 || row == height - 1 || col == 0 || col == width - 1;
if (on_edge) {
picture += '#';
} else {
picture += ((row + col) % 2 == 0) ? '+' : ' ';
}
}
picture += '\n';
}
return picture;
}Output for ex2(6, 3):
######
#+ + #
######The middle row has + at columns 1 and 3, because their sums are 2 and 4, which are even. The cell at column 2 has sum 3, so it is a space.
Exercise 3: Count the dark cells
Write a function int count_dark(int rows, int cols) that returns how many cells have an even sum of row and column. Check it for a 3 by 3 grid and a 2 by 5 grid.
Show the answer
int count_dark(int rows, int cols) {
int dark = 0;
for (int row = 0; row < rows; ++row) {
for (int col = 0; col < cols; ++col) {
if ((row + col) % 2 == 0) {
++dark;
}
}
}
return dark;
}Running it prints count_dark(3, 3) as 5 and count_dark(2, 5) as 5. A 3 by 3 grid has 5 even cells, because the even cells are the four corners and the centre. A 2 by 5 grid has 5 even cells. The first row has 3 of them (columns 0, 2 and 4), and the second row has 2 (columns 1 and 3).
Exercise 4: Predict a one-cell grid
Predict the output of grid(1, 1) from the grid art section. Then run it.
Show the answer
+---+
| |
+---+The function makes out_rows equal to 3 and out_cols equal to 5. Line 0 is a wall line, line 1 is a cell line, and line 2 is another wall line. Columns 0 and 4 are multiples of 4, so they hold + on wall lines and | on cell lines.
Exercise 5: Find the bug
The function below is meant to draw a box with a solid border. Its output is wrong. Find the line that causes the problem and explain it.
std::string ex5_broken(int width, int height) {
std::string picture;
for (int row = 0; row < height; ++row) {
for (int col = 0; col < width; ++col) {
bool on_edge = row == 0 || row == height || col == 0 || col == width - 1;
picture += on_edge ? '#' : '.';
}
picture += '\n';
}
return picture;
}Running ex5_broken(5, 3) prints:
#####
#...#
#...#Show the answer
The bug is row == height in the on_edge test. The rows run from 0 to height - 1, so row never equals height. The bottom border is never drawn, and the last row is filled like the inside. The fix is to compare with height - 1, as the other edge tests do:
bool on_edge = row == 0 || row == height - 1 || col == 0 || col == width - 1;With that change, the same call prints:
#####
#...#
#####Summary
- A grid is a pair of loops, and the rule for each cell is a test on its row and column.
(row + col) % 2 == 0alternates from one cell to the next, which gives a checkerboard.- Test for the border with
row == 0,row == height - 1,col == 0andcol == width - 1, and check the border first. - Put the rule for one cell in its own function, so the picture code only has to loop.
- For a grid with lines, the picture is about twice the number of cells, plus one.
- Use
std::ssizewhen comparing anintwith a container's size.