— John C. Maxwell
Ah, static class members! They pop up in every C++ project. In most cases, however, their use is not justified. I try to avoid static members as much as possible, and so should you. Here’s why.
Let’s start by revisiting the mother of all static member text book examples. It demonstrates that by using the ‘static’ keyword, state can be shared among all instances of a class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
class Employee { public: Employee(/* employee data */) { ++s_employeeCount; // Initialize rest of employee members. // .. } ~Employee() { --s_employeeCount; } [[nodiscard]] static int employeeCount() { return s_employeeCount; } private: static int s_employeeCount; }; // Definition outside of class necessary. int Employee::s_employeeCount; TEST(static_member_employee, trivial) { { Employee john; Employee mary; Employee jim; ASSERT_EQ(3, Employee::employeeCount()); } ASSERT_EQ(0, Employee::employeeCount()); } |
The reasoning in these text books usually goes like this: The total number of employees obviously doesn’t belong to a particular employee instance. Such information is shared by all instances, so let’s use ‘static’ here.
Don’t write code like this. It’s so flawed I don’t even know where to start. But I’ll try anyway.
If you believe that something is not the responsibility of an instance, you should not add this responsibility to the instance’s class either. Calling the static member function like so:
1 2 3 |
auto employeeCount = Employee::employeeCount(); |
Is still a call on Employee, so it’s still a responsibility of the class. Instead, assign the responsibility to keep track of employees to a higher-level entity, like a Company or an Employer class, which manages Employees in a container, a std::vector, for instance. Such a higher-level class would provide a regular instance method that returns the total number of employees:
1 2 3 4 5 6 7 8 9 10 11 12 |
class Company { public: void onboard(const Employee& employee); void offboard(const Employee& employee); // ... [[nodiscard]] int employeeCount() const { return m_employees.size(); } private: std::vector<Employee> m_employees; }; |
With this new design, in order to find out how many employees work for a company, you do the natural thing — you ask the company, not a static member function of class Employee. (Also note that in class Company, employeeCount is declared as ‘const’ to signify that it doesn’t update any data. There’s no such thing as ‘const’ for a static member function.)
What else is uncool about the original Employee class? Like every non-const static member ‘s_employeeCount’ needs to be defined exactly once outside the class, which is typically done in the corresponding .cpp file.
Further, manually incrementing and decrementing the count is cumbersome — the std::vector in Company will do this automatically for us. On top of that, the original design is not exception-safe. Imagine that initialization code in the constructor body of Employee throws an exception after the count has already been updated. The Employee instance will have never officially existed but the count indicates otherwise. You need to add extra exception handling code around the count increment to safeguard against such cases.
The original design is also sub-optimal as static members impede unit testing. After every unit test case, you have to make sure that the static data is cleaned up (or reinitialized) for the next unit test case. Since the static data is usually private, one often has to add extra public static initialization or setter methods to do the job, thus complicating the class interface even more. In general, static member functions can’t be mocked (at least if you’re using Google Mock) so it might be difficult if not impossible to simulate particular behavior of a static method to obtain code/branch coverage in a client component.
So are there any legitimate uses of static members in C++ classes? In my view, there’s only one: public (and protected) symbolic constants:
1 2 3 4 5 6 7 8 9 |
class Calendar { public: static constexpr int daysPerYear = 365; static constexpr int daysPerLeapYear = 366; explicit Calendar(int year); //... }; |
Private constants or private utility methods that do not touch instance data are usually better defined as anonymous namespace members within the class’ .cpp file — this avoids cluttering up the class definition in the header file with things that bear no relevance to the user of a class (aka implementation details).
But what about public utility functions that don’t use instance data? Shouldn’t they be declared ‘static’?
1 2 3 4 5 6 7 8 |
class MathUtils { public: // ... [[nodiscard]] uint64_t factorial(uint64_t n) const; // ... }; |
The official answer is probably ‘yes’, and there are compilers and tools (like clang-tidy) that suggest that you should declare a method ‘static’ in such situations. These days, however, I rather tend to ignore/suppress such warnings based on this reasoning: if a function is declared ‘public’, it’s part of a class’ interface. The fact that its implementation doesn’t touch instance data (today) is just an implementation detail that doesn’t need to be conveyed to the user of a class. I’ll pick a regular (virtual) method that I can mock over a static method any day:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class IMathUtils { public: // ... [[nodiscard]] virtual uint64_t factorial(uint64_t n) const = 0; // ... }; class MathUtils : public IMathUtils { public: // ... [[nodiscard]] uint64_t factorial(uint64_t n) const override; // ... }; |
This design clearly differentiates between interface and implementation and achieves loose coupling between the class and its clients. At runtime, clients can replace one implementation with another (e. g. optimized for speed, with caching, with logging, a mock and so on) without having to change the client code. All this would not have been possible had ‘factorial’ been declared ‘static’.
To sum it up:
- There’s only one legitimate use-case for static members: symbolic constants
- State shared by all instances of a class should not be shared in that class, but rather in an instance of a higher-level class
- Static members prevent late (i. e. runtime) binding
- Static data and static member functions make unit testing difficult