RSS | GitHub |
Coding guidelines often introduce a special notation to distinguish member variables from function parameters. This notation is not known to the compiler, so it cannot detect violations. At best we can write rules for static analysis tools, such as clang-tidy, to detect violations.
But there is an alternative supported by the compiler, namely nested classes.
A class can be declared within another class. A class declared within another is called a nested class. The name of a nested class is local to its enclosing class. The nested class is in the scope of its enclosing class.
-- C++ Standard N4849, section $[$class.nest$]$/1
A nested class is a member and as such has the same access rights as any other member.
C++ Standard N4849, section $[$class.access.nest$]$/1
A nested class is simply a class that is declared inside another class.
A nested class is a member of the outer class, so it has full access rights to the members of the outer class. The obverse is not true – the outer class only has public access right to the members of the nested class.
A nested class is not an inner class. The nested object does not know which enclosing object it belongs to, unless the this
pointer of the enclosing object is passed to the nested object.
Let us revisit the problem we are trying to address.
A declaration of a name in a nested declarative region hides a declaration of the same name in an enclosing declarative region.
C++ Standard N4849, section $[$basic.scope.hiding$]$/1
C++ allows variable shadowing where we can use a variable name in an inner scope even though another variable with the same name already exists in an outer scope.
Shadowing, however, can lead to undetected defects in member functions where parameters may shadow member variables.
The problem is even more subtle for constructors because there is no shadowing in member initializer lists. The compiler can correctly choose between constructor parameters and member variables with the same name, but once you get into the body of the constructor the usual shadowing applies again.
Usual workarounds are to rely on compiler warnings, const parameters, the this
pointer, or naming conventions to address this problem, but each have their own set of limitations.
Instead, we are going to create a dedicated “namespace” for member variables by putting them into a nested class.
Our member variable is now accessed with member.value
.
Unlike naming conventions such as a prefixed m_
or a suffix underscore, where we accidentally could name a function parameter m_value
or value_
, we cannot accidentally name the function parameter member.value
. In other words, we have effectively created a naming convention for member variables that can be verified by the compiler.
We can pick any legal variable name for our nested class, so we could have used m.value
if we wanted to have a syntax close to the m_value
convention. However, we should prefer expressive names over abbreviations because it reduces the cognitive load on new project members.
There is no run-time overhead of using a nested member class. The compiler will generate the same code as an equivalent class without a nested class, even when optimization is turned off.
An aggregate is an array or a class with
– no user-declared or inherited constructors,
– no private or protected direct non-static data members,
– no virtual functions,
– no virtual, private, or protected base classes.
C++ Standard N4849, section $[$dcl.init.aggr$]$/1
In the previous example we used aggregate initialization of the member
variable, which saves us from writing a constructor for the nested class. Although that works fine in many cases, there are situations were we cannot rely on aggregate initialization.
When aggregate initialization is not possible, we need to add a constructor to the nested member
class. Narrowing also prevents aggregate initialization.
In the remainder we are going to look at some special cases.
Let us say we want to use an std::atomic
If we have an atomic member variable, then we need to use the same kind of initialization.
Notice the extra set of brackets in the aggregate initialization above.
A static data member shall not be a direct member of an unnamed $[$...$]$ class
C++ Standard N4849, section $[$class.static.data$]$/2
We cannot add static members to an anonymous nested class.
This means that we need to pull all our static member variables out into the enclosing class.
If we want to hide the implementation details of our member variables, then the pimpl idiom is a natural extension of the nested class notation.
Now the member variables have to be accessed as member->value
, but otherwise the notation remains the same.
Sometimes template classes need to select member variables at compile-time. This can be done using a template nested class with specializations.
Consider std::span
. When using a dynamic extent, the span size is determined at construction time and thus needs to be stored as a member variable. When using a fixed extent, the span size is encoded into the type, and the size
member variable is unnecessary.
A simplified span could look as follows, ignoring empty base optimization as the real span needs other member variables.
Notice that we maintain the member.
notation, although we need to use a nested member function to access member variables that could be omitted.