| Lesson 7 |
Member functions |
| Objective |
Understand how to declare a member function. |
C++ Member Functions (How to Declare and Use Member Functions)
The member function declaration is included in the struct declaration and is invoked by using access methods for structure members. The idea is that the functionality required by the struct data type should be directly included in the struct declaration. This improves the encapsulation of the ADT by packaging its operations directly with its data representation. Lesson 6 established what objects are and how they relate to classes. This lesson examines the syntax for declaring member functions — the operations that make a struct or class more than a passive data container — and uses the ch_stack running example to show each function type in practice.
Member Function Declaration Syntax
The Declaration Format
A member function declaration follows the same syntax as any C++ function declaration but appears inside the struct or class body. The general form is:
// Declaration only (prototype) — body defined elsewhere
return_type function_name(parameter_list);
// Inline definition — body defined immediately inside the struct/class
return_type function_name(parameter_list) {
// function body
}
Every member function declaration consists of four elements. The return type specifies what value the function produces — void for functions that perform an action without returning a value, or a specific type (char, bool, int) for functions that compute and return a result. The function name is a valid C++ identifier that names the operation being performed — descriptive names like push, pop, and reset make the struct's interface self-documenting. The parameter list — enclosed in parentheses — specifies zero or more typed input values the function accepts; an empty parenthesis pair () means the function takes no arguments. The body — enclosed in braces {} — contains the statements that implement the operation, with direct access to all data members of the struct through the implicit this pointer.
Inline Definitions Inside the struct Body
When the function body appears immediately after the parameter list inside the struct declaration — as in the ch_stack example — the function is defined inline. Inline definitions are appropriate for short, simple functions whose implementation is obvious from reading the declaration. Short read-only functions like empty() and full() are natural candidates for inline definition — the entire body fits on one line and the intent is immediately clear. Longer, more complex functions — like a function that performs error recovery or complex state transitions — belong in a separate source file, declared inside the struct and defined outside using the scope resolution operator ::. Module 3 covers out-of-line member function definitions in detail.
The ch_stack ADT — Member Functions in Practice
Here is an example of a stack ADT that declares the various functions associated with the stack as member functions. The following modernized version of ch_stack strips the legacy syntax highlighting artifacts and applies C++23 best practices — static constexpr for the capacity constant, enum class for the sentinel values, const on all read-only functions, and [[nodiscard]] on functions whose return values should not be discarded:
#include <cassert> // required for assert()
static constexpr int MAX_LEN = 40;
struct ch_stack {
// data representation
char s[MAX_LEN];
int top = static_cast<int>(Sentinel::EMPTY);
enum class Sentinel : int { EMPTY = -1, FULL = MAX_LEN - 1 };
// operations represented as member functions
void reset() noexcept {
top = static_cast<int>(Sentinel::EMPTY);
}
void push(char c) {
assert(top != static_cast<int>(Sentinel::FULL)); // precondition
s[++top] = c;
}
[[nodiscard]] char pop() {
assert(top != static_cast<int>(Sentinel::EMPTY)); // precondition
return s[top--];
}
[[nodiscard]] char top_of() const { // const: does not modify state
assert(top != static_cast<int>(Sentinel::EMPTY));
return s[top];
}
[[nodiscard]] bool empty() const noexcept {
return top == static_cast<int>(Sentinel::EMPTY);
}
[[nodiscard]] bool full() const noexcept {
return top == static_cast<int>(Sentinel::FULL);
}
};
reset() — Initializing State
reset() takes no parameters and returns void — it performs an action (initializing the stack to empty) without computing a result. It sets top to the sentinel value EMPTY (-1), which the other member functions recognize as the "stack is empty" state. noexcept specifies that this function cannot throw an exception — it only assigns an integer, so the guarantee is warranted. reset() is a mutator — it modifies the struct's state — so it is not const.
push() — Adding to the Stack
push(char c) takes one parameter (the character to push) and returns void. Before pushing, it asserts that the stack is not full — assert(top != FULL) is a precondition check that documents the function's requirement and enforces it in debug builds. The assert() macro from <cassert> evaluates its argument and aborts the program with a diagnostic message if the condition is false. This is the correct mechanism for programming errors (calling push() on a full stack is a bug, not a recoverable runtime condition); exceptions are more appropriate for recoverable runtime errors. After confirming the precondition, push() increments top and stores the character. push() is a mutator — not const.
pop() — Removing from the Stack
pop() takes no parameters and returns char — the character removed from the top of the stack. It asserts the stack is not empty before accessing s[top], then returns the character while decrementing top in a single expression (s[top--]: post-decrement returns the value of top before decrementing). [[nodiscard]] on pop() is essential — calling pop() and discarding the return value silently loses data and is almost certainly a programming error. The compiler will warn when [[nodiscard]] is present and the return value is ignored. pop() is a mutator — not const.
top_of(), empty(), full() — Read-Only Operations
top_of(), empty(), and full() are all read-only operations — they inspect the stack's state without modifying it. All three are declared const, which enforces this contract at the language level: a const member function cannot modify any non-mutable data member of the struct. This allows these functions to be called on const ch_stack objects and through const ch_stack& references — the standard way to pass objects to functions that should not modify them. All three are also marked [[nodiscard]] because calling empty() or full() and ignoring the result is always a programming error, and calling top_of() without using the returned character is similarly wasteful. empty() and full() are additionally noexcept — they only compare integers and cannot throw.
Why Member Functions Improve Over Free Functions
The C Approach — Functions with Struct Pointers
Before C++ member functions, the C approach to associating operations with a data structure was to write separate functions that accept a pointer to the struct as their first parameter:
// C-style: data and operations are separate
struct ch_stack_c {
char s[40];
int top;
};
// Separate functions — must pass the struct explicitly
void reset_stack(ch_stack_c* st) { st->top = -1; }
void push_stack(ch_stack_c* st, char c) { st->s[++st->top] = c; }
char pop_stack(ch_stack_c* st) { return st->s[st->top--]; }
bool stack_empty(const ch_stack_c* st) { return st->top == -1; }
// Usage — verbose, no guarantee that the right struct is passed
ch_stack_c stk;
reset_stack(&stk);
push_stack(&stk, 'A');
char c = pop_stack(&stk);
The C++ Approach — Functions Inside the Type
C++ member functions eliminate the explicit struct pointer parameter. The struct itself is the implicit first argument — the
this pointer — passed automatically by the compiler. The function lives inside the type declaration, making the association between data and operations explicit in the source code:
// C++ style: data and operations are declared together
ch_stack stk;
stk.reset(); // cleaner — no explicit pointer, no naming mismatch
stk.push('A');
char c = stk.pop();
The dot syntax (
stk.push('A')) makes the relationship between the object and the operation explicit — push is an operation on a
ch_stack, not a standalone function that happens to take one. This syntactic clarity is what Bjarne Stroustrup meant when he described classes as a way to write programs in terms of the domain — the code reads the way the problem reads.
Encapsulation Benefit — Data and Operations Together
Packaging the member functions directly with the data representation in the struct declaration produces two concrete benefits. First, the struct definition is self-documenting — reading the struct declaration tells you everything about what the type can do, not just what data it holds. Second, the member functions have direct access to all data members without requiring any parameter — push() accesses top and s[] directly, without being told which stack to operate on. This is the encapsulation improvement mentioned in the opening paragraph: "packaging its operations directly with its data representation."
assert() — Precondition Checking in Member Functions
How assert() Works
The assert() macro from <cassert> evaluates a boolean expression at runtime. If the expression is true, execution continues normally. If the expression is false, assert() prints a diagnostic message to standard error — including the failed expression, the source file name, and the line number — then calls std::abort() to terminate the program. This behavior makes assert() appropriate for programming errors: conditions that should never be false in a correct program but that the programmer wants to verify during development and testing. In ch_stack, calling pop() on an empty stack is a programming error — the caller should have checked empty() before calling pop(). The assert enforces this contract and provides a clear diagnostic when it is violated.
Release Build Behavior — NDEBUG
Asserts are disabled in release builds by defining the preprocessor macro NDEBUG. When NDEBUG is defined, every assert() call expands to nothing — zero runtime overhead. Most build systems define NDEBUG automatically for release configurations. This means asserts are active during development and testing — where they catch programming errors — and inactive in production — where the overhead would be paid by end users for conditions that should not occur in correct code. The ch_stack member functions rely on this pattern: push() asserts the stack is not full, and pop() asserts it is not empty, providing safety during development without production overhead.
Modern Alternative — C++26 Contracts
C++26 introduces a formal contracts facility that replaces the
assert() macro pattern with language-level precondition and postcondition syntax. A
push() function with contracts would declare its precondition directly on the function signature:
// C++26 contracts (proposed syntax)
void push(char c)
[[pre: top != static_cast<int>(Sentinel::FULL)]]
{
s[++top] = c; // precondition guaranteed by the contract
}
Contracts integrate with the type system, can be checked at different levels (observe, enforce, ignore), and are visible in the function's interface rather than buried in the body. For current C++23 code,
assert() remains the standard mechanism.
Inline vs Out-of-Line Member Functions
Inline Definitions — Short Functions in the Class Body
All six ch_stack member functions are defined inline — their bodies appear immediately inside the struct declaration. This is appropriate for these functions because each body is short (one to four statements), the implementation is obvious, and the performance benefit of inline expansion is meaningful for small frequently-called functions. The compiler may expand inline function calls directly at the call site, eliminating the overhead of a function call — stack frame setup, parameter passing, and return — for trivial operations like empty() that evaluate to a single comparison.
Out-of-Line Definitions — The Scope Resolution Operator
When a member function body is too long or complex to define comfortably inside the struct declaration, the function is declared inside the struct and defined outside using the scope resolution operator
:::
struct ch_stack {
// Declaration inside the struct
void push(char c); // body defined elsewhere
};
// Definition outside the struct — uses :: to identify membership
void ch_stack::push(char c) {
assert(top != static_cast<int>(Sentinel::FULL));
s[++top] = c;
}
The
ch_stack:: prefix tells the compiler that this
push() definition belongs to the
ch_stack struct, not to the global namespace. Module 3 covers the scope resolution operator and out-of-line member function definitions in full detail.
const Member Functions — Read-Only Operations
What const Means on a Member Function
The const qualifier after the closing parenthesis of a member function's parameter list — bool empty() const — declares that the function does not modify the object's state. The compiler enforces this: any attempt to modify a non-mutable data member inside a const member function produces a compile error. const member functions can be called on const objects and const references; non-const member functions cannot. Correctly marking read-only functions const is not optional — it is part of the correct interface design for any type with a meaningful const-correct interface.
Which ch_stack Functions Should Be const
In the legacy ch_stack code, none of the member functions are marked const. Applying const-correctness to the modernized version: reset() modifies top — not const. push() modifies top and s[] — not const. pop() modifies top — not const. top_of() only reads s[top] — should be const. empty() only reads top — should be const. full() only reads top — should be const. This is why the modernized ch_stack marks these three functions const: it expresses the accurate design intent and allows these functions to be used wherever a const ch_stack& appears in the program.
In the next lesson, you will learn how to use member functions — how to call them through objects and how the member access syntax works in practice.

