ClassIdioms

The Null Object Pattern

A Null Object encapsulates a do nothing behavior inside an object. It is often pretty comfortable to use a neutral object.

 ClassIdioms

A Null Object

  • encapsulates the do nothing behavior inside an object.
  • supports the workflow without conditional logic.
  • hides the special use cases from the client.

Honestly, there is not much to write about the Null Object. Let me, therefore, give you an example, using a Null Object.

Strategized Locking

Assume you write code such as a library, which should be used in various domains, including concurrent ones. To be on the safe side, you protect the critical sections with a lock. If your library now runs in a single-threaded environment, you have a performance issue because you implemented an expensive synchronization mechanism that is unnecessary. Now, strategized locking comes to your rescue.

Strategized locking is the idea of the Strategy Pattern applied to locking. This means putting your locking strategy into an object and making it into a pluggable component of your system.

 

Rainer D 6 P2 500x500Modernes C++ Mentoring

Be part of my mentoring programs:

  • "Fundamentals for C++ Professionals" (open)
  • "Design Patterns and Architectural Patterns with C++" (open)
  • "C++20: Get the Details" (open)
  • "Concurrency with Modern C++" (starts March 2024)
  • Do you want to stay informed: Subscribe.

     

    There are two typical ways to implement strategized locking: run-time polymorphism (object orientation) or compile-time polymorphism (templates).
    Both ways improve the customization and extension of the locking strategy, ease the maintenance of the system, and support the reuse of components. Also, implementing the strategized locking at run-time or compile-time differ in various aspects.

    Advantages

    Run-Time Polymorphism

    • Allows it to configure the locking strategy during run time
    • Is easier to understand for developers who have an object-oriented background

    Compile-Time Polymorphism

    • Has no abstraction penalty
    • Has a flat hierarchy.

    Disadvantages

    Run-Time Polymorphism

    • Needs an additional pointer or reference indirection
    • It may have a deep derivation hierarchy

    Compile-Time Polymorphism

    • It may generate very wordy error messages.

    Implementation based on Run-Time Polymorphism

     The program strategizedLockingRuntime.cpp presents three different mutexes.

     

    // strategizedLockingRuntime.cpp
    
    #include <iostream>
    #include <mutex>
    #include <shared_mutex>
    
    class Lock {
    public:
        virtual void lock() const = 0;
        virtual void unlock() const = 0;
    };
    
    class StrategizedLocking {
        Lock& lock;                                  // (1)
    public:
        StrategizedLocking(Lock& l): lock(l){
            lock.lock();                             // (2)
        }
        ~StrategizedLocking(){
            lock.unlock();                           // (3)
        }
    };
    
    struct NullObjectMutex{                          
        void lock(){}
        void unlock(){}
    };
    
    class NoLock : public Lock {                     // (4)
        void lock() const override {
            std::cout << "NoLock::lock: " << '\n';
            nullObjectMutex.lock();
        }
        void unlock() const override {
            std::cout << "NoLock::unlock: " << '\n';
             nullObjectMutex.unlock();
        }
        mutable NullObjectMutex nullObjectMutex;     // (9)
    };
    
    class ExclusiveLock : public Lock {              // (5)
        void lock() const override {
            std::cout << "    ExclusiveLock::lock: " << '\n';
            mutex.lock();
        }
        void unlock() const override {
            std::cout << "    ExclusiveLock::unlock: " << '\n';
            mutex.unlock();
        }
        mutable std::mutex mutex;                    // (10)
    };
    
    class SharedLock : public Lock {                 // (6)
        void lock() const override {
            std::cout << "        SharedLock::lock_shared: " << '\n';
            sharedMutex.lock_shared();                // (7)
        }
        void unlock() const override {
            std::cout << "        SharedLock::unlock_shared: " << '\n';
            sharedMutex.unlock_shared();              // (8)
        }
        mutable std::shared_mutex sharedMutex;        // (11)
    };
    
    int main() {
        
        std::cout << '\n';
        
        NoLock noLock;
        StrategizedLocking stratLock1{noLock};
        
        {
            ExclusiveLock exLock;
            StrategizedLocking stratLock2{exLock};
            {
                SharedLock sharLock;
                StrategizedLocking startLock3{sharLock};
            }
        }
        
        std::cout << '\n';
        
    }
    

     

    The class StrategizedLocking has a lock (line 1). StrategizedLocking models scoped locking and, therefore, locks the mutex in the constructor (line 2) and unlocks it in the destructor (line 3). Lock is an abstract class and defines all derived classes’ interfaces. These are the classes NoLock (line 4), ExclusiveLock (line 5), and SharedLock (line 6). SharedLock invokes lock_shared (line 7) and unlock_shared (line 8) on its std::shared_mutex. Each of these locks holds one of the mutexes NullObjectMutex (line 9), std::mutex (line 10), or std::shared_mutex (line 11). NullObjectMutex is a noop placeholder. The mutexes are declared as mutable. Therefore, they are usable in constant member functions such as lock and unlock.

    Implementation based on Compile-Time Polymorphism

     The template-based implementation is quite similar to the object-oriented-based implementation.

     

    // strategizedLockingCompileTime.cpp
    
    #include <iostream>
    #include <mutex>
    #include <shared_mutex>
    
    
    template <typename Lock>
    class StrategizedLocking {
        Lock& lock;
    public:
        StrategizedLocking(Lock& l): lock(l){
            lock.lock();
        }
        ~StrategizedLocking(){
            lock.unlock();
        }
    };
    
    struct NullObjectMutex {
        void lock(){}
        void unlock(){}
    };
    
    class NoLock{                                         // (1)
    public:
        void lock() const {
            std::cout << "NoLock::lock: " << '\n';
            nullObjectMutex.lock();
        }
        void unlock() const {
            std::cout << "NoLock::unlock: " << '\n';
             nullObjectMutex.lock();
        }
        mutable NullObjectMutex nullObjectMutex;
    };
    
    class ExclusiveLock {                                  // (2)
    public:
        void lock() const {
            std::cout << "    ExclusiveLock::lock: " << '\n';
            mutex.lock();
        }
        void unlock() const {
            std::cout << "    ExclusiveLock::unlock: " << '\n';
            mutex.unlock();
        }
        mutable std::mutex mutex;
    };
    
    class SharedLock {                                     // (3)
    public:
        void lock() const {
            std::cout << "        SharedLock::lock_shared: " << '\n';
            sharedMutex.lock_shared();
        }
        void unlock() const {
            std::cout << "        SharedLock::unlock_shared: " << '\n';
            sharedMutex.unlock_shared();
        }
        mutable std::shared_mutex sharedMutex;
    };
    
    int main() {
    
        std::cout << '\n';
        
        NoLock noLock;
        StrategizedLocking<NoLock> stratLock1{noLock};
        
        {
            ExclusiveLock exLock;
            StrategizedLocking<ExclusiveLock> stratLock2{exLock};
            {
                SharedLock sharLock;
                StrategizedLocking<SharedLock> startLock3{sharLock};
            }
        }
        
        std::cout << '\n';
    
    }
    

     

    The programs strategizedLockingRuntime.cpp and strategizedLockingCompileTime.cpp produce the same output:

    strategizedLocking

    The locks NoLock (line 1), ExclusiveLock (line 2), and SharedLock (line 3) have no abstract base class. The consequence is that StrategizedLocking can be instantiated with an object that does not support the right interface. This instantiation would end in a compile-time error. This loophole is closed with C++20.

    The Concept BasicLockable

    Instead of template <typename Lock> class StrategizedLocking you can use the
    concept BasicLockable: template <BasicLockable Lock> class StrategizedLocking. This means that all used locks have to support the concept BasicLockable. A concept is a named requirement, and many concepts are already defined in the C++20 concepts library. The concept BasicLockable is only used in the text of the C++20 standard. Consequently, I define and use the concept BasicLockable in the following improved implementation of the strategized locking at compile time.

    // strategizedLockingCompileTimeWithConcepts.cpp
    
    #include <iostream>
    #include <mutex>
    #include <shared_mutex>
    
    template <typename T>                     // (1)
    concept BasicLockable = requires(T lo) {
        lo.lock();
        lo.unlock();
    };
        
    template <BasicLockable Lock>             // (2)
    class StrategizedLocking {
        Lock& lock;
    public:
        StrategizedLocking(Lock& l): lock(l){
            lock.lock();
        }
        ~StrategizedLocking(){
            lock.unlock();
        }
    };
    
    struct NullObjectMutex {
        void lock(){}
        void unlock(){}
    };
    
    class NoLock{
    public:
        void lock() const {
            std::cout << "NoLock::lock: " << '\n';
            nullObjectMutex.lock();
        }
        void unlock() const {
            std::cout << "NoLock::unlock: " << '\n';
             nullObjectMutex.lock();
        }
        mutable NullObjectMutex nullObjectMutex;
    };
    
    class ExclusiveLock {
    public:
        void lock() const {
            std::cout << "    ExclusiveLock::lock: " << '\n';
            mutex.lock();
        }
        void unlock() const {
            std::cout << "    ExclusiveLock::unlock: " << '\n';
            mutex.unlock();
        }
        mutable std::mutex mutex;
    };
    
    class SharedLock {
    public:
        void lock() const {
            std::cout << "        SharedLock::lock_shared: " << '\n';
            sharedMutex.lock_shared();
        }
        void unlock() const {
            std::cout << "        SharedLock::unlock_shared: " << '\n';
            sharedMutex.unlock_shared();
        }
        mutable std::shared_mutex sharedMutex;
    };
    
    int main() {
    
        std::cout << '\n';
        
        NoLock noLock;
        StrategizedLocking<NoLock> stratLock1{noLock};
        
        {
            ExclusiveLock exLock;
            StrategizedLocking<ExclusiveLock> stratLock2{exLock};
            {
                SharedLock sharLock;
                StrategizedLocking<SharedLock> startLock3{sharLock};
            }
        }
        
        std::cout << '\n';
    
    }
    

     

    BasicLockable in line (1) requires that an object lo of the type T that it has to support the member functions lock and unlock. The use of the concept is straightforward. Instead of typename, I use the concept BasicLockable in the template declaration of StrategizedLocking (line 2).

    What’s Next?

    To use your user-defined type in a range-based for-loop, you have to implement the Iterator Protocol. Let me discuss the details in my next post.

     

     

     

     

    Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Kris Kafka, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Stephen Kelley, Kyle Dean, Tusar Palauri, Dmitry Farberov, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, moon, Philipp Lenk, Hobsbawm, and Charles-Jianye Chen.

    Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.

    My special thanks to Embarcadero
    My special thanks to PVS-Studio
    My special thanks to Tipi.build 
    My special thanks to Take Up Code
    My special thanks to SHAVEDYAKS

    Seminars

    I’m happy to give online seminars or face-to-face seminars worldwide. Please call me if you have any questions.

    Standard Seminars (English/German)

    Here is a compilation of my standard seminars. These seminars are only meant to give you a first orientation.

    • C++ – The Core Language
    • C++ – The Standard Library
    • C++ – Compact
    • C++11 and C++14
    • Concurrency with Modern C++
    • Design Pattern and Architectural Pattern with C++
    • Embedded Programming with Modern C++
    • Generic Programming (Templates) with C++
    • Clean Code with Modern C++
    • C++20

    Online Seminars (German)

    Contact Me

    Modernes C++ Mentoring,

     

     

    0 replies

    Leave a Reply

    Want to join the discussion?
    Feel free to contribute!

    Leave a Reply

    Your email address will not be published. Required fields are marked *