10 - static methods
Just like static
fields, static
methods are not tied to any particular instance of the class. They are essentially global (free) functions in the class namespace.
static
methods can not access non-static class fields.static
methods can not have any qualifiers (const
,volatile
,&
,&&
). Since they do not work on any object, there is no point in declaring any object-specific behavior.
Named constructors
One particularly good use of static
methods is named constructor idiom. In some languages, constructors can have specific names. In C++ and many other languages that support OOP, constructors have no names - they just use the name of the class. This can sometimes lead to unclear code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class temperature { public: temperature(double value) : value(value) {} // other methods... private: double value; }; void some_func() { // What unit is this? // Celsius? Fahrenheit? Kelvin? temperature t = 0.0; } |
Clarity of such code can be improved by implementing named constructors (or actually faking it since C++ has no such feature):
make the constructor
private
supply
public
static
methods that return an object
Now any external code that uses the class is much clearer:
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
constexpr double celsius_to_fahrenheit(double celsius) { return 1.8 * celsius + 32; } constexpr double fahrenheit_to_celsius(double fahrenheit) { return (fahrenheit - 32) / 1.8; } // https://en.wikipedia.org/wiki/Absolute_zero constexpr double absolute_zero = -273.15; constexpr double celsius_to_kelvin(double celsius) { return celsius - absolute_zero; } constexpr double kelvin_to_celsius(double kelvin) { return kelvin + absolute_zero; } class temperature { public: static temperature from_kelvin(double kelvin) { return temperature(kelvin); } static temperature from_celsius(double celsius) { return from_kelvin(celsius_to_kelvin(celsius)); } static temperature from_fahrenheit(double fahrenheit) { return from_celsius(fahrenheit_to_celsius(fahrenheit)); } // other methods... private: temperature(double kelvin) : kelvin(kelvin) {} double kelvin; }; void some_func() { // now any object creation is very clear auto t1 = temperature::from_kelvin(0); auto t2 = temperature::from_celsius(100); } |
Singletons
A common pattern that uses static
methods and private
constructor is singleton, that is a class that is designed on purpose to only allow one object of its type.
This can sometimes be useful to limit interactions with the state that should exist only once such as program-wide configuration object, but the pattern can very quickly become an antipattern if overused. The more singletons a program has, the more interactions with global state (complicates reasoning about the program) and the more troublesome testing becomes. Code with lots of singletons is also much harder to refactor because one place of code can couple very arbitrary, different parts of the program, trigerring a cascade of required changes if the singleton is to be modified.
Canonical singleton in C++ looks as follows (this is sometimes called Meyer's Singleton as this form appeared in his book):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class singleton { public: singleton& get_instance() { static singleton instance(/* args... */); return instance; } // remove every other possibility of object creation // copy/move ctor/operator= are explained in RAII chapter singleton(const singleton&) = delete; singleton& operator=(const singleton&) = delete; singleton(singleton&&) = delete; singleton& operator=(singleton&&) = delete; // public functions that offer actual functionality... private: singleton(/* ctor arguments... */); // state... }; |
Empty classes with static
methods
Sometimes you may encounter a code like this:
1 2 3 4 5 6 7 8 9 10 |
class math { public: static double sin(double rad); static double cos(double rad); // more trigonometry... // all metods are static // no data members }; |
Such code is against C++ idioms. It's typically written by people who are too used to OOP and don't understand that in C++ there is no need to create a class. Some languages force OOP style and (probably the most disliked language by C++ programmers) Java is a primary example (hello world in Java requires to write a class).
The correct code for the example above is:
1 2 3 4 5 6 |
namespace math { double sin(double rad); double cos(double rad); // more trigonometry... } |
Notice that after refactoring, function calls would remain the same: math::sin(angle)
etc. If the class had any private
methods as implementation detail, they can be moved to an anonymous namespace.
Corner cases
Pretty much no one writes code like this so it's more of a fun fact than any rule worth remembering.
Why ban static data members in these cases? I don't really know, my guess is that it simplifies the job of compiler writers.
Why are such definitions even possible? Perhaps some consistency reason and maybe backwards compatibility, but I remember that once being able to define an enum
inside a function was useful. Note that lambda expressions technically each create a new type and by design they are intended to be defined locally.