Why this matters
Every program has small helper functions: one that squares a number, one that formats money, one that checks whether a string is empty. Beginners often put each helper wherever it happens to fit, and after a few weeks the code is hard to read. Some helpers sit inside a class even though they never touch the class's data. Others sit outside a class even though they need data that only the class holds.
This lesson gives you a simple test for where a helper belongs. It also introduces two C++ words you will see in real code: a static method and a utility class. Both are ways to group helper functions. You will see when each one is the right tool and when it is not.
This lesson uses only functions and classes. The rules for passing values in and out of functions are in the earlier lessons of this module, so they are not repeated here.
The idea: a free function needs no object
Start with the simplest kind of function. It is written outside any class, it has a name, it takes inputs, and it returns an output. In C++ people call this a free function, because it is not tied to an object.
Press Run. The output is:
square(6) = 36square takes a number and returns a number. It does not remember anything between calls. Give it the same input and it gives the same output, and it does not need a box of stored data to do its job. This is the test that matters most: a free function needs only what you pass to it.
When a helper belongs on an object
An object is a value that holds its own data. In C++ the data is stored in member variables, and the functions that work on that data are called member functions or methods. A rectangle object might hold a width and a height. The area of the rectangle is a calculation that needs both of those numbers.
The output is:
small area: 6
big area: 40Look at the function area. It reads width_ and height_. It cannot know which numbers to use unless it is called on a particular rectangle, so it must be a member of the class. The call small.area() means "compute the area of this rectangle." The word const after the parentheses promises that area will not change the rectangle. You will learn more about const in the read-only parameters lesson.
Here is the rule that connects both examples.
- If a function needs data stored in one object, make it a member function of that object.
- If a function needs only its inputs, keep it free.
The square function needs only its input, so it is free. The area function needs the stored width and height, so it is a member.
Static member functions
Sometimes a helper belongs to a class by name, but it does not need any particular object. It may build an object, or it may do a calculation that does not depend on stored data. For that case C++ has the static member function. You write the keyword static in front of the function inside the class. The function then belongs to the class itself, not to any one object.
You call a static member function by writing the class name, two colons, and the function name. The two colons are called the scope operator. They say "look inside this class for this name."
The output is:
object: 20
static: 20Both lines print 20. The first call uses an object, r, and reads the object's stored numbers. The second call uses only the class name and passes the numbers in. A static member function has no object behind it, so it has no this. this is the C++ word for "the object this function was called on." A static function is not called on an object, so there is nothing for this to point to. You will see the error for this in the Common mistakes section.
A static function can still be useful inside the class. It can be used to build the object in a controlled way. That is the next example.
A worked example: a bank account
This example brings the ideas together. It uses four kinds of code and shows where each one lives.
The output is:
Ada: $0.00
Linus: $15.50
accounts opened: 2Read the program in four parts.
withdrawis a member function. It reads and changesbalanceCents_, which is stored in the object. It has to be a member, because the balance belongs to one account.formatMoneyis a free function. It takes an amount in cents and returns a text string. It does not need any account. Any part of the program can call it with any number, so it stays outside the class.withZeroBalanceis a static member function. It is a second way to create an account. You call it before any account exists, asBankAccount::withZeroBalance("Ada"). It belongs in the class because it is about creating accounts, but it does not need an existing account to run.accountsOpenedis a static member variable. Its line starts withinline static. The wordstaticmeans there is one copy of this number for the whole class, not one copy per account. Each time a constructor runs, the constructor adds 1 to this shared number. The output shows 2 because two accounts were created. The wordinlinelets the number be defined inside the class body. Without it, C++ before version 17 would require a separate definition outside the class.
Notice what withZeroBalance does with its result. It returns an object by value, which means the caller receives a complete BankAccount. The constructor is the same one used for b. Calling the constructor from inside a static function is common and normal. The std::move calls hand each string over without copying it. The output would be the same with copies, so you can treat them as an efficiency detail for now.
Utility classes and namespaces
A utility class is a class that holds only static functions. Nobody builds an object from it. It is just a named box of helper functions. Some C++ code bases do this, and you will see it in older code. Here is an example.
The output is:
0
5.5The line MathUtils() = delete; says that nobody may create a MathUtils object. The word delete here does not mean free memory. It means "this function does not exist, and using it is a compile error." This blocks code like MathUtils m; from compiling, which makes the intent clear to the next reader.
Modern C++ has a simpler way to write the same helpers. A namespace is a named group of functions and other names. It is not a class, so it cannot be built into an object at all. A namespace is the usual choice for a group of free helpers.
The output is:
0
5.5The two versions give the same results. The namespace version is shorter, needs no MathUtils() = delete; line, and is the form most C++ programmers expect to see for a set of helpers. The namespace also works for functions that are not part of any class, such as formatMoney in the bank example. Use a namespace when the functions are a group of plain tools. Use a static member function when the function is about one class, such as creating an object of that class.
Here is the choice in one table.
| Where the helper lives | Use it when |
|---|---|
| Free function | It needs only its inputs and is not about one class |
| Member function | It reads or changes the data of one object |
| Static member function | It is about one class, but needs no particular object |
| Namespace of functions | It is a group of plain tools, such as math or text helpers |
A third example: a factory with a private constructor
A static member function is often used as a factory. A factory is a function whose job is to create and return an object. The useful part is that the class can hide its constructor, so the only way to create an object is through the factory. The factory can check the input before it creates anything.
This example stores a temperature in Celsius. It has two factories, one for Celsius and one for Fahrenheit. It refuses any temperature below absolute zero, which is the lowest possible temperature in physics, about -273.15 degrees Celsius.
The output is:
boiling: 212 F
room: 20 C
error: below absolute zeroThe constructor is private, which means code outside the class cannot call it. The line Temperature u = 20.0; would not compile, and neither would Temperature t(10); from main. Callers must use Temperature::fromCelsius or Temperature::fromFahrenheit. The check for absolute zero sits in the constructor, so every path that creates a Temperature runs the check. A Temperature object that exists is always a valid one.
The try and catch blocks handle the error. When the constructor throws, the catch block runs and prints the message. A thrown exception is a signal that an error happened. Here you only need to see that the bad value stops the object from being created.
Common mistakes
Using a member from a static function
A static member function has no object behind it, so it cannot read the object's member variables. Here is a small class that breaks that rule.
class Rectangle {
public:
Rectangle(int w, int h) : width_(w), height_(h) {}
static int area() { return width_ * height_; }
private:
int width_;
int height_;
};GCC 15 with -std=c++20 -Wall reports:
error: invalid use of member ‘Rectangle::width_’ in static member function
error: invalid use of member ‘Rectangle::height_’ in static member functionThe fix is to remove static and call the function on an object, or to pass the two numbers in as parameters.
Using this in a static function
The pointer this only exists inside a function that runs on an object. Writing this-> in a static function is an error.
class Counter {
int count = 0;
public:
static void useThis() { this->count = 1; }
};GCC 15 reports:
error: ‘this’ is unavailable for static member functionsCalling a member function through the class name
The class name is not an object. A call such as Counter::add() only works when add is static. If add is a normal member function, the call fails.
class Counter {
int count = 0;
public:
void add() { count++; }
};
int main() {
Counter::add();
}GCC 15 reports:
error: cannot call member function ‘void Counter::add()’ without objectThe fix is to create an object first, as in Counter c; c.add();.
Making a static function for something that needs the object
A common beginner habit is to make every function static, because it avoids typing an object name. This breaks down when the function needs the object's data. A static area() cannot know which rectangle to measure. The error message above shows the same problem in a different form. If the function needs stored data, make it a normal member.
Forgetting the second half of a static member variable
A static member variable needs one definition. The line inline static int accountsOpened = 0; in the bank example provides that definition in C++17 and later. Before C++17, the same variable needed a second line outside the class, such as int BankAccount::accountsOpened = 0;. If you copy older code that has this line and the compiler reports an error about an undefined reference, check for a missing definition.
Practice
Exercise 1
Decide where each function should live. Write one sentence for each explaining why.
double circleArea(double radius)that returns the area of a circle with that radius.double balance()on a bank account that returns the account's stored balance.bool isBlank(const string& text)that returns true if the text has no characters.
Show the answer
- A free function. It needs only its input, the radius.
- A member function of the account. It reads data stored inside one object.
- A free function, or a function in a namespace of text helpers. It needs only its input.
Exercise 2
Predict the output of this program. Then check by running it.
Show the answer
The output is:
10
12The inner call Doubler::twice(3) returns 6. The outer call then doubles 6 and returns 12.
Exercise 3
Rewrite this utility class as a namespace with the same two functions. Keep the same names and behaviour.
class StringUtils {
public:
StringUtils() = delete;
static bool isEmpty(const std::string& s) { return s.empty(); }
static std::string shout(const std::string& s) { return s + "!"; }
};Show the answer
namespace stringutils {
bool isEmpty(const std::string& s) {
return s.empty();
}
std::string shout(const std::string& s) {
return s + "!";
}
}Call the functions as stringutils::isEmpty and stringutils::shout. The namespace needs no constructor line, because a namespace cannot be turned into an object.
Exercise 4
Add a static factory named Point::origin() to this class. It should return a point at x = 0 and y = 0. Then write a line in main that creates the origin and prints its x value.
class Point {
public:
Point(int x, int y) : x_(x), y_(y) {}
int x() const { return x_; }
private:
int x_;
int y_;
};Show the answer
Add this inside the class:
static Point origin() {
return Point(0, 0);
}In main:
Point o = Point::origin();
cout << o.x() << endl;The printed value is 0.
Exercise 5
Explain in two or three sentences why Temperature::fromCelsius(-300) in the factory example throws an error, even though the class has no code in main that checks the value.
Show the answer
The check is in the private constructor. The factory calls that constructor, and the constructor throws invalid_argument when the value is below -273.15. The value is checked whenever any Temperature is created, so main does not need its own check.
Summary
- A free function needs only its inputs. Keep it outside any class when it is not about one object.
- A member function reads or changes data stored in one object. Use it when the function needs that data.
- A static member function belongs to the class, not to an object. Call it as
ClassName::name(...). It has nothis. - A utility class holds only static helpers. A namespace of plain functions is the usual modern choice for the same job.
- A static factory creates objects of its class, often with a private constructor so that every object is checked when it is made.