1.0 Concept
Properties are a programming language construct that provide encapsulated instances of the property type
with setter and getter methods to access that value. C++ does not provide properties, but this code shows
that can be done with templates and some specialization magic.
There are some important reasons for using properties:
-
Provide thread-safe operations for the encapsulated instances. Property access methods may take
and release locks as needed, so users of the class don't have to do that.
-
Provide logging of access to the encapsulated instances. Again, access methods provide
whatever logging is needed.
-
Enforce application specific constraints on operations that may be executed on the
encapsulated instances, e.g., authorization, styling, security checks, ...
If the Property class provides virtual functions for access, derived classes may simply provide whatever
their application needs.
Fig 1. CppProperties Class Diagram
Fig 2. Properties Test Code
Fig 3. CppProperties Test Output
2.0 Design
This design of properties uses an inheritance hierarchy, shown in Figure 1., to implement the various
design aspects needed for property behaviors, as described in the concept statement, above.
The first base class, PropContainer<T>, holds the encapsulated instance t ε T and provides
protected virtual methods:
-
virtual void set(const T& t)
-
virtual T& get()
The purpose of these methods is to manage access to the encapsulated type.
The PropertyBase<T>
class inherits those protected methods and uses them to implement the user interface methods:
-
PropertyBase(const T& t)
-
PropertyBase& operator=(const T& t)
-
void operator()(const T& t)
-
T operator()()
The first three of these methods call
void PropContainer<T>::set(const T& t)
and the last calls
T& PropContainer<T>::get().
Note that the set(const T& t) method copy assigns t to the
encapsulated instance, e.g., makes a copy so users can't modify the
inner instance through that reference. Also note that the operator()()
method returns a copy of the inner instance, for the same reason. For large types, those
copies will have preformance implications, but are necessary to ensure application defined
constraints on operations are enforced and any changes, for thread-safe properties, happen
only in a locked critical section.
Since Property<T> is not a T, it doesn't have T's methods.
If Property<T>
derived from T, that would provide those methods, but then it would be very difficult
(maybe impossible) to enforce application constraints and provide consistent logging and
thread safety. So, I elected to take the design route described below.
The class PropertyOps<T> provides methods for frequently occurring types:
- fundamental type operations
- operations for STL sequential containers
- operations for STL associative containers
Each of these types require different interfaces, so the PropertyOps<T> class provides
specializations for each of these, based on custom type traits: is_fundamental,
is_stl_seq_container, and is_stl_assoc_container. The selection of those specializations uses some
template metaprogramming constructs provided by C++14 and C++17. The Property.h and CustomContTypeTraits.h
files provide some notes about that.
Whenever it can, PropertyOps<T> uses the protected
PropContainer<T> reference-based methods for performance,
only using instance copy operations when it must to maintain thread-safety or application constraints.
For types not included in PropertyBase<T>, e.g.,
Widget, applications can retreive
a copy of the encapsulated type using T PropertyBase<T>::operator()(),
use the copy's methods, and set the modified instance with
void PropertyBase<T>::operator()(const T& t) method.
That, obviously has performance and convenience issues, but would probably be worthwhile for
multi-threaded environments or applications needing specific logging operations.
For frequently used types, a developer may always elect to add another PropertyOps
specialization. That will probably be easier than for STL containers, since those class interfaces
are likely to be smaller, simpler, and easier to implement.
Three classes derive from PropertyOps<T>:
-
Property<T> imposes no constraints and does no locking.
-
TS_Property<T> provides locking, by overriding empty public
lock() and unlock() methods,
inserting calls to methods lock() and unlock()
in the PropContainer<T> class.
-
Log_Properties<T> provides logging by overriding protected
methods void set(const T& t) and
T& get() methods.
The Property<T> and TS_Property<T> classes
have been implemented and are part of this repository. Log_Property<T>
is planned and should appear soon.
A final observation about this design: note how property operations are factored into
single-responsibility classes, e.g.:
-
PropContainer provides virtual functions to manage its encapsulated
instance tεT, providing the flexibility needed by applications to enforce constraints
and thread safety.
-
PropBase<T> defines the primary user interface.
-
PropertyOps<T> adds methods for widely used types.
-
The most derived classes
Property<T>,
TS_Property<T>, and
Log_Property<T> simply override
PropContainer<T> methods to suit the current application.
PropContainer<T> sets up the flexibility infrastructure, and
Property<T>,
TS_Property<T>, and
Log_Property<T> use that for their application.
The Single-Responsibility Principle is the queen - most important - of all the design principles.
3.0 Build
CppProperties was built with Visual Studio Community Edition - 2019 and tested on Windows 10.
4.0 Status
All of the classes except
Log-Property<T> have been implemented. The code has not been used
in any major application yet, so there may be some latent undetected errors.
Two of the STL containers, std::stack<T> and std::queue<T>
are adapters of other containers. They have a special behavior - provide access only to their end or ends.
That is, they cannot be iterated and have no begin() and end()
methods. They need their own PropertyOps<T> processing. I have not done that
yet, so using them will cause compilation failure. Since you can use the std::deque<T>
as a stack or queue, I will not do that for awhile.
This facility needs a better demonstration of typical use. I plan to provide that, but I won't get
to that for a while.
All of this is fairly complex. I probably would not use this facility except for the use-cases cited in
the Concept section.