Chapter 17 · Fixed-Size Arrays: std::array and C-Style Arrays
Exercise · Chapter 17

Chapter 17 — Fixed-Size Arrays: Tic-Tac-Toe Referee

You will build a stateless Tic-Tac-Toe referee — a small library of pure functions that inspect a game board and answer questions like "who won?" and "is this a draw?" — plus two utility functions that demonstrate the two most important C-style-array lessons.

The board is represented as a 2D std::array:

C++
using Board = std::array<std::array<char, 3>, 3>;

Each cell holds 'X', 'O', or ' ' (empty). All board-inspecting functions receive the board by const reference (notes 17.3) — a cornerstone rule for passing fixed-size arrays without unnecessary copies.

The exercise is split into two independent parts:

Part A — std::array 2D (tasks 1–5). You write helper functions that detect wins in rows, columns, and diagonals, then combine them into winner() and isDraw(). The test suite feeds hand-built boards for every win type — row, column, main diagonal, anti-diagonal — plus the draw and in-progress cases, so there is no place for a lucky coincidence to pass the grader.

Part B — C-style arrays + decay + pointer arithmetic (tasks 6–7). Two focused utility functions. sumScores demonstrates array decay (notes 17.8): the const int scores[] parameter silently becomes const int*, so sizeof(scores) inside the function yields the pointer size, not the array size. firstNegative demonstrates pointer arithmetic with a half-open range [begin, end) (notes 17.9) — the same conceptual model that powers std::vector::begin()/end() and every standard-library algorithm you will meet in Chapter 18.

Why Tic-Tac-Toe? Because std::array<std::array<char,3>,3> is small enough to be readable yet large enough to practice every 2D traversal pattern — and the game logic is simple enough that you can verify the output mentally. The real lesson is in the shapes: how to pass a 2D array by const ref, how to index it, and what happens when you cross the boundary from std::array into C-style territory.


Your tasks

  1. Count a mark (countMark). Walk every cell of board with nested range-for loops and count how many cells equal ch. Constraints: pass board by const Board&; use const auto& row for the outer loop variable so you do not copy each inner array.

  2. Check if full (isFull). Return true if no ' ' cell remains. Delegate to countMark — one call, one comparison. No loops needed here.

  3. Detect a winning row or column (rowWinner, colWinner). For each row r, check whether board[r][0] == board[r][1] == board[r][2] and that the cell is not ' '. Similarly for each column c. Return the winning character or ' '. Use index-based for loops (r = 0..2 or c = 0..2).

  4. Detect a winning diagonal (diagWinner). Check the main diagonal (0,0)→(1,1)→(2,2) and the anti-diagonal (0,2)→(1,1)→(2,0). Return the winner or ' '. No loops needed — just three-cell comparisons.

  5. Combine the helpers (winner, isDraw). winner() calls rowWinner, colWinner, and diagWinner in sequence, returning the first non-' ' result. isDraw() returns true when winner() is ' ' and isFull() is true.

  6. Sum a C-style array (sumScores). Sum scores[0..count-1] with a plain index loop. The key lesson is in the decay: const int scores[] in the parameter list is identical to const int* scores after the compiler processes it — the length is completely lost. That is why the caller passes count separately. Do not use sizeof(scores) inside the body.

  7. Walk a half-open range (firstNegative). Walk [begin, end) with for (const int* p { begin }; p != end; ++p) and return the first pointer where *p < 0, or end if none. Constraint: advance with ++p, not p[i] or p + i — the goal is to feel pointer arithmetic directly.

Success criteria

  • countMark(kEmpty, 'X') == 0 — empty board has no X marks
  • countMark(kDraw, 'X') + countMark(kDraw, 'O') + countMark(kDraw, ' ') == 9 — counts must cover every cell
  • isFull(kDraw) == true — all 9 cells occupied
  • rowWinner(kEmpty) == ' ' — all-empty rows do not count as wins
  • rowWinner(kXRow2) == 'X' — detects the bottom row, not just the top
  • diagWinner(kXMainDiag) == 'X' and diagWinner(kOAntiDiag) == 'O' — both diagonals detected
  • winner(kDraw) == ' ' — a draw board has no winner
  • isDraw(kXRow0) == false — a won board is not a draw even when full
  • sumScores(negs, 4) == 10 — negative values in the array must be summed correctly
  • firstNegative(data, data) == data — empty range must return end immediately (no crash)
  • firstNegative(allPositive, end) == end — "not found" must return the end sentinel
Concepts practiced
  • std::array<T,N> basics — construction, indexing, size() (notes 17.1, 17.2)
  • Passing std::array by const reference — the right default for read-only array parameters (notes 17.3)
  • Nested std::arraystd::array<std::array<char,3>,3> as a 2D board; row/column indexing (notes 17.13)
  • Aggregate initialization with nested bracesBoard {{ {row}, {row}, {row} }} (notes 17.4)
  • using type aliasesBoard makes the 2D type readable (ch 10)
  • C-style array decayconst int scores[] parameter is really const int*; sizeof gives pointer size (notes 17.8)
  • Pointer arithmetic++p, *p, half-open [begin, end) range convention (notes 17.9)
  • Nested range-for loops for 2D traversal (reused from ch 16 range-for)
  • Index-based for loops for row/column wins (reused from ch 8)
  • Const referencesconst Board&, const int* (reused from ch 12)
  • Type aliasesusing Board = ... (reused from ch 10)
  • CS6340 bridge: LLVM's BasicBlock::begin()/end() follows the same half-open pointer/iterator convention as firstNegative.

Constraints
  • Allowed: std::array, C-style arrays, range-for loops, index-based for loops, const references, type aliases (using), if/else, return, and all features from chapters ≤ 17.
  • Forbidden: <algorithm> or any std:: algorithm/iterator object (Chapter 18 — not taught yet), new[] / delete[] (Chapter 19), lambdas (Chapter 20).
  • Required idioms:
    • All board parameters must be passed by const Board& (never by value).
    • firstNegative must advance with ++p, not index arithmetic.
    • sumScores must use the count parameter (not sizeof) for the loop bound.

Build & run locally
shell
make            # compile-check starter/tictactoe.cpp (warning-clean)
make test       # grade your code -> RED until the TASK blocks are filled in
make solution   # run the grader against the reference (to see what passing looks like)
make clean      # remove build artifacts

make test is the grader — it supplies its own main and calls every function across many fixed inputs.


Hints
Task 1 — nested range-for and const auto&
C++
int count { 0 };
for (const auto& row : board)   // const auto& avoids copying the inner array
    for (char cell : row)
        if (cell == ch)
            ++count;
return count;

The outer const auto& is important: without it, each row would be a copy of std::array<char,3>, a pointless allocation. Always bind the outer loop variable by reference when looping over an std::array of arrays (notes 17.3 / 17.13).

Task 3 — index loop for row/column wins

For rows:

C++
for (int r { 0 }; r < 3; ++r) {
    char first { board[static_cast<std::size_t>(r)][0] };
    if (first != ' '
        && first == board[static_cast<std::size_t>(r)][1]
        && first == board[static_cast<std::size_t>(r)][2])
        return first;
}
return ' ';

Checking first != ' ' prevents an all-empty row from being mistakenly called a winner. The same pattern applies to columns — just transpose the indices.

Task 4 — diagonal comparisons (no loops needed)
C++
char mid { board[1][1] };           // centre cell shared by both diagonals

// main diagonal: (0,0) – (1,1) – (2,2)
if (mid != ' ' && board[0][0] == mid && board[2][2] == mid)
    return mid;

// anti-diagonal: (0,2) – (1,1) – (2,0)
if (mid != ' ' && board[0][2] == mid && board[2][0] == mid)
    return mid;

return ' ';

Checking mid != ' ' short-circuits immediately if the centre is empty.

Task 5 — winner/isDraw orchestration
C++
char winner(const Board& board) {
    char w { rowWinner(board) };   if (w != ' ') return w;
    w = colWinner(board);          if (w != ' ') return w;
    w = diagWinner(board);         if (w != ' ') return w;
    return ' ';
}

bool isDraw(const Board& board) {
    return winner(board) == ' ' && isFull(board);
}
Task 6 — sumScores: plain index loop on a decayed pointer
C++
int total { 0 };
for (int i { 0 }; i < count; ++i)
    total += scores[i];
return total;

scores[i] is valid even though scores is a pointer — subscripting a pointer is identical to *(scores + i) (notes 17.9). Remember: count is the ONLY reliable source of length here; sizeof(scores) yields the pointer size, not the array length.

Task 7 — firstNegative: pointer arithmetic with ++p
C++
for (const int* p { begin }; p != end; ++p) {
    if (*p < 0)
        return p;
}
return end;

++p advances by one int (the compiler scales by sizeof(int) for you — notes 17.9). *p dereferences to read the element. Returning end when no negative is found is the standard "not found" convention for half-open ranges — the caller checks result != end.


Stretch goals
  • Add printBoard(const Board&) that renders the board to std::cout with ASCII borders (uses only ch ≤ 17 features).
  • Make winner() return std::optional<char> so the absence of a winner is explicit rather than encoded as ' ' (notes ch 12 std::optional).
  • Replace the 'X'/'O'/' ' char with a scoped enum class Cell { X, O, Empty } and update all functions — prevents the caller from passing arbitrary characters (Chapter 13).
  • Write a flat-board variant using FlatBoard = std::array<char, 9> with an index mapping idx(r, c) = r*3 + c, as described in notes 17.13 ("Flattening"), and add a second set of helper functions for it.
  • In a separate main.cpp, write a simple two-player console loop that lets two humans play; use winner() and isDraw() from this library to decide when to stop (uses only ch ≤ 17 features).
starter/tictactoe.cpp C++
// Chapter 17 — Fixed-Size Arrays · Project: Tic-Tac-Toe Referee   (STARTER)
// ─────────────────────────────────────────────────────────────────────────────
// Fill in the seven TASK blocks below.  Each maps 1:1 to a task in the README
// and to a declaration in ../tictactoe.h.  The bodies currently return WRONG
// placeholders so the file compiles immediately — that is why `make test` is
// RED right now.  Your job is to turn it GREEN by implementing the real logic.
//
//     make build         compile your code      (should already work)
//     make test          grade it               (RED until you fill these in)
//     make solution      run the grader against the reference if you get stuck
//
// ── Scope reminder ────────────────────────────────────────────────────────────
// Only chapter ≤ 17 features are needed:
//   std::array, C-style arrays, range-for loops (ch 16), index loops (ch 8),
//   const references (ch 12), function templates are NOT required here —
//   the Board typedef hides the type complexity.
//   FORBIDDEN: <algorithm>, new[], any C++20 feature.

#include "../tictactoe.h"

// ─────────────────────────────────────────────────────────────────────────────
// PART A — std::array 2D (tasks 1–5)
// ─────────────────────────────────────────────────────────────────────────────

// ─── TASK 1: count a mark across the whole board ─────────────────────────────
// countMark — walk every cell of `board` and count how many equal `ch`.
//
// KEY TERM: std::array<std::array<char,3>,3> is a 2D "array of arrays" (notes
// 17.13). board[r] is the r-th row (an std::array<char,3>); board[r][c] is one
// cell. Walk rows with an outer range-for, then columns with an inner range-for.
//
// Constraint: pass board by const reference — never copy a 2D array (notes 17.3).
// The signature already specifies `const Board&`; keep it.
//
//   >>> YOUR CODE HERE <<<
//
int countMark(const Board& board, char ch)
{
    (void)board; (void)ch;
    return 0;   // placeholder — always reports 0 marks (wrong for non-empty boards)
}
// ─────────────────────────────────────────────────────────────────────────────

// ─── TASK 2: check whether the board is full ─────────────────────────────────
// isFull — return true when no ' ' (empty) cell remains.
//
// Hint: delegate to countMark. One call, one comparison — no loops needed here.
//
//   >>> YOUR CODE HERE <<<
//
bool isFull(const Board& board)
{
    (void)board;
    return false;   // placeholder — always reports not full (wrong for full boards)
}
// ─────────────────────────────────────────────────────────────────────────────

// ─── TASK 3: scan for a winning row or column ────────────────────────────────
// rowWinner / colWinner — detect a complete row or column.
//
// KEY TERM: a *row* win means board[r][0] == board[r][1] == board[r][2] AND that
// character is not ' ' (an all-empty row does NOT count as a win). Similarly for
// columns: board[0][c] == board[1][c] == board[2][c].
//
// Use an outer loop over r (or c) and compare the three cells directly.
// Return the winning character, or ' ' if no row (or column) is complete.
//
//   >>> YOUR CODE HERE <<<
//
char rowWinner(const Board& board)
{
    (void)board;
    return ' ';   // placeholder — reports no row winner (wrong when a row is won)
}

//   >>> YOUR CODE HERE <<<
//
char colWinner(const Board& board)
{
    (void)board;
    return ' ';   // placeholder — reports no column winner
}
// ─────────────────────────────────────────────────────────────────────────────

// ─── TASK 4: check the two diagonals ─────────────────────────────────────────
// diagWinner — return the winning character if the main diagonal or the
// anti-diagonal is complete, or ' ' if neither is.
//
// KEY TERM: main diagonal — (0,0),(1,1),(2,2). Anti-diagonal — (0,2),(1,1),(2,0).
// No loops needed; just compare three cells each time.
//
//   >>> YOUR CODE HERE <<<
//
char diagWinner(const Board& board)
{
    (void)board;
    return ' ';   // placeholder — always reports no diagonal winner
}
// ─────────────────────────────────────────────────────────────────────────────

// ─── TASK 5: combine the helpers ─────────────────────────────────────────────
// winner — ask each helper in turn; return the first non-' ' result, else ' '.
// isDraw  — no winner AND board is full.
//
// Keep the logic in the helpers — winner() and isDraw() are just orchestrators.
//
//   >>> YOUR CODE HERE <<<
//
char winner(const Board& board)
{
    (void)board;
    return ' ';   // placeholder — always reports no winner
}

bool isDraw(const Board& board)
{
    (void)board;
    return false;   // placeholder — always reports not a draw
}
// ─────────────────────────────────────────────────────────────────────────────

// ─────────────────────────────────────────────────────────────────────────────
// PART B — C-style arrays + decay + pointer arithmetic (tasks 6–7)
// ─────────────────────────────────────────────────────────────────────────────

// ─── TASK 6: sum a C-style array (and observe decay) ─────────────────────────
// sumScores — sum scores[0..count-1].
//
// KEY TERM: ARRAY DECAY (notes 17.8).  The parameter `const int scores[]` is
// SYNTACTIC SUGAR for `const int* scores` — the compiler converts it for you.
// Inside this function body, `scores` is a POINTER; sizeof(scores) yields the
// pointer size (8 bytes on most 64-bit platforms), NOT the array size.  The
// README shows this in the "decay lesson" section.
//
// Use a plain index loop `for (int i = 0; i < count; ++i)`.
// Do NOT call sizeof(scores) — just use count.
//
//   >>> YOUR CODE HERE <<<
//
int sumScores(const int scores[], int count)
{
    (void)scores; (void)count;
    return 0;   // placeholder — always returns 0 (wrong for non-empty arrays)
}
// ─────────────────────────────────────────────────────────────────────────────

// ─── TASK 7: walk a half-open range with pointer arithmetic ──────────────────
// firstNegative — return a pointer to the first negative element in [begin,end),
// or `end` if none.
//
// KEY TERM: POINTER ARITHMETIC (notes 17.9).  The half-open range [begin,end)
// convention: `begin` points at the first element, `end` points ONE PAST the last.
// Walk with `for (const int* p { begin }; p != end; ++p)` and test `*p < 0`.
//
// Return the pointer where the first negative lives, or `end` if the range has
// no negative element.  The caller checks `result != end` to know if one was found.
//
// Constraint: use `++p` (not p[i] or p+i) to advance — the 17.9 lesson.
//
//   >>> YOUR CODE HERE <<<
//
const int* firstNegative(const int* begin, const int* end)
{
    (void)begin; (void)end;
    return end;   // placeholder — pretends no negative was found (wrong when one exists)
}
// ─────────────────────────────────────────────────────────────────────────────
Run
Submit
Run in your browser — coming soon For now: copy or download the files and use make test locally (see “Build & run locally” above).