Implementing Lazy Loading with Proxy Design Pattern in C++

Introduction

I attempt to discuss about Proxy Design from a different approach that you may see from other online contents.

First, I dive deep into its the concepts that are basic benefits of the design pattern: lazy loading and fast compilation. Then I explain in generals the main ideas for Proxy pattern.

For implementation, I implement the proxy that both creates and deletes Real Service class, thus having composition relationship. RefactoringGuru page uses aggregate relationship.

Finally, I add function pointer to illustrate how it saves memory in call stacks by using function pointer. This is an extended benefit of lazy loading.

Lazy Loading Concept

Lazy loading is a technique used in C/C++ to defer the loading of data or libraries until they are actually needed, instead of loading them all at once when the program starts. This can be particularly useful for programs that have large data sets or depend on large libraries that may not be needed for all program execution paths.

When lazy loading is used, data or libraries are not loaded into memory until they are actually needed by the program. This can reduce the program's memory footprint, since it does not need to load everything into memory at once. It can also improve the program's startup time, since the program does not need to load everything before it can start executing.

One common use of lazy loading is in dynamic linking. When a program uses dynamic linking, it loads libraries at runtime rather than linking them statically at compile time. With lazy loading, the libraries are not loaded until they are actually needed by the program. This can be more efficient than loading all of the libraries at once, particularly if the program only uses a small subset of the available libraries.

Another example of lazy loading is in the loading of large data sets. Instead of loading the entire data set into memory at once, the program can load only the portions that are needed at a given time. This can reduce the program's memory usage and improve its performance.

In summary, lazy loading is a technique used in C/C++ to defer the loading of data or libraries until they are actually needed, rather than loading them all at once. This can reduce the program's memory footprint and improve its performance.

C++ building concepts

  • Translational unit:
    • In C/C++, a translation unit is the basic unit of compilation. It is a source code file, along with any header files that it includes, after preprocessing has been applied. The resulting text is known as a translation unit, which is then compiled into an object file.
  • Internal linkage: Identifiers with internal linkage are only accessible within the same translational unit. Variables and functions can be given internal linkage by using the static keyword.
  • External linkage: Identifiers with external linkage can be accessed from any part of the program, including other translation units. Functions and global variables have external linkage by default, but can be made static to limit their scope to the current translation unit.
  • The compilation follows the steps:
    • create preprocess outputs
    • building object files from translational units that includes cpp files
    • link object files to an executable file
    • We will apply those knowledge to test the dependency of code changes on compilation.
C:\> g++ -E *.cpp          --> this command creates preprocess files.
C:\> g++ -c *.cpp          --> this command creates object files
C:\> g++ -o main *.o       --> this command links object files to exe file

Reference: a very good blog detailing C/C++ compilation could be found here.

Proxy Design Pattern Concepts

Proxy design pattern employs a proxy class that provides call to a real service class. The service class provides actual logic implementation. The call to the service class is only when necessary, usually for the first time only. Data is loaded from database by real service class could be saved at different address space by proxy. Access to real service class could be further restricted, and controlled by Proxy. In addition, changes in service class will not effect the proxy class.

Therefore, proxy design pattern has 3 benefits: lazy loading and caching data, access control and faster build.

  • "Implementation or Service class": implements the logic and load data from database when called by Proxy. Initialized once when needed by Proxy.
  • Proxy class:
    • controls access to real class, could provide additional logic to allow access Implementation class
    • performs lazy loading of data. Initializes Service class, calls service class to load data and saved data to "cache" for fast access while the proxy is running.
    • used as substitute for real class

Proxy Implementation

Full proxy pattern implementation with proxy interface

The following program illustrate a service class, RealShop and a proxy class, ProxyShop.

Both inherit IShop interface.

The client class is Customer.

The program works similar to how a customer come to a shop to buy some commodity. If the product is available at the shop, the customer would get it rightaway. Otherwise, the seller will call warehouse to bring up the product. In our case, the ProxyShop will initialize RealShop and load the product to its cache, or saved product object to memory.

In addition, other implementation of proxy pattern makes uses of inheritance as suggested in Refactoring Guru. The main idea is

  • to delay the initialization of the real data until needed.
  • date is saved to memory, and used by proxy so there is no need to query data again
  • the proxy class hold a reference or pointer to the actual class the implements the logic
  • both proxy class and actual class implements a common interface.
  • client class only calls the interface functions.

The following class diagram illustrates a proxy pattern designed program.

  • main is the client that uses the service provided by the interface IProxy::provideService function.
  • both ShopProxy and RealShop implements IProxy interface.
  • ShopProxy has an composition relation to RealShop, meaning it has a pointer member to RealShop and its existence is managed by ShopProxy
    • RealShop is created and data is provided to RealShop. In this case, the composition relation may defer creation of RealShop until it is needed.
    • RealShop is deleted when Proxy is deleted.
  • ShopProxy::provideService initializes RealShop if Factory is not instantiated. Otherwise, RealShop is called to provide the main logic. Thus RealShop pointer is also called to implement the provideService function.

References:

shop_interface.h

// needed preprocessor to prevend loop include
#ifndef ISHOP
#define ISHOP
#include "string"

class IShop{
    public:
    IShop(){};
    virtual void provideService(std::string product) = 0;
    virtual ~IShop(){};
};
#endif

shop_proxy.h

#include "shop.h"
#include "shop_interface.h"

class ShopProxy : public IShop {
    public:
    ShopProxy();
    virtual ~ShopProxy();
    virtual void provideService(std::string product) override;
    private:
    RealShop * realShop_ = nullptr;    // safe initialization of pointer
};

shop_proxy.cpp

#include "shop_proxy.h"
#include "iostream"

void ShopProxy::provideService(std::string product) {
    if ( realShop_ == nullptr  ) {                  
        std::cout<<"Proxy creates RealShop on request"<<std::endl;     
        realShop_ = new RealShop();                               //create RealShop, call RealShop to do service
        realShop_ -> provideService(product);
    } 
    else{
        std::cout<<"Proxy can provide service"<<std::endl;
        // RealShop is available in Proxy's memory.
        // don't have to reload, just run logic
        realShop_ ->provideService(product);                              
    }
};


ShopProxy::ShopProxy() 
{
    std::cout<<"**ShopProxy Constructor**"<<std::endl;
}

ShopProxy::~ShopProxy() 
{
    std::cout<<"**ShopProxy Destructor**"<<std::endl;
    delete realShop_;
    realShop_ = nullptr;     // safe delete of member pointer
}

shop.h

#include "shop_interface.h"
#include "string"

class RealShop: public IShop {
    public:
    RealShop();
    virtual ~RealShop();
    virtual void provideService(std::string product) override ;
    private:
    void collectMaterial();
};

shop.cpp

#include "shop.h"
#include "iostream"
using namespace std;

void RealShop::provideService(string product) {
    cout<<"    shop is building "<<product<<" in warehouse"<<endl;
    collectMaterial();
}

void RealShop::collectMaterial() {
    cout<<"    collect material to build product"<<endl;
}

RealShop::RealShop(){
    cout<<"    RealShop constructor"<<endl;
}

RealShop::~RealShop(){
    cout<<"    RealShop destructor"<<endl;
}

main.cpp

  • Note that main.cpp has to include interface proxy_interface.h and the proxy class declaration shop_proxy.h
#include "shop_interface.h"
#include "shop_proxy.h"

int main()
{
    std::string requestProduct1 = "REQUEST 1: a doll";
    IShop * myShopProxy = new ShopProxy();
    // myShopProxy creates RealShop
    myShopProxy->provideService(requestProduct1);
    // myShopProxy already has RealShop, and can provide service.
    std::string requestProduct2 = "REQUEST 2: a toy";
    myShopProxy->provideService(requestProduct2);

    // delete myShopProxy will delete RealShop and data saved in memory
    delete myShopProxy;

    // create new proxy
    std::string requestProduct3 = "REQUEST 3: a fan";
    IShop * myShopProxy2 = new ShopProxy();
    // myShopProxy has to create new RealShop again and reload data.
    myShopProxy2->provideService(requestProduct3);

    delete myShopProxy2;
}

Analysis

Lazy loading benefit

When running the program, it shows that the RealShop instance is only created once. Instead the proxy's saved RealShop pointer is called to do the work in the second time. When proxy is deleted, RealShop is deleted as well.

  • How proxy pattern performs lazy loading:
    • ShopProxy class acts as a lazy-loading proxy for the Shop class
    • The ShopProxy class initializes the Factory object on demand.
    • Lazy loading is implemented by initializing the realShop_ member pointer of the ShopProxy class to nullptr in the constructor, and creating a new RealShop instance when the collectMaterial function is called for the first time
    • The creation of the ShopProxy class is less costly compared to the creation of the RealShop. In practice, RealShop initialization could involves querying data from database. and ShopProxy could have save the data to cache, by implementing a private data structure.
    • With RealShop already created and data alread saved by ShopProxy, we don't have to initialze RealShop again nor loading data from remote database.
Fast compilation benefit
  • What are the translational units for the above programs:
    • shop.cpp: This file defines the implementation of the Shop class, and is compiled into an object file (e.g. shop.o).
    • shop_proxy.cpp: This file defines the implementation of the ShopProxy class, and is compiled into an object file (e.g. shop_proxy.o)
    • main.cpp: This file defines the main function and creates instances of ShopProxy. It includes the headers for shop_proxy.h which includes the header for shop.h. This file is compiled into an object file (e.g. main.o).
    • During the linking phase, the object files are linked together to create the final executable file (e.g. my_program).
  • How proxy pattern improves compiling time.
    • If an extra function is added to RealShop, rebuilding only shop.cpp and linking the newly built object file with already built object files WILL UPDATE the program

For the following program, changes to the implementation class, RealShop, is isolated. Thus the compilation process only requires rebuilding the translation unit of RealShop class, or shop.cpp

We made drastic change to Shop:

  • Add a private function specialService
  • Add a public variable static const bool special_ = true;
  • Add a private variable customerName_, year_

We rebuild only RealShop.

#include "shop_interface.h"
#include "string"

class RealShop: public IShop {
    public:
    RealShop();
    virtual ~RealShop();
    virtual void provideService(std::string product) override ;
    static const bool specialOrder_ = true;            // newly added
    private:
    void collectMaterial();
    void specialService(std::string customerName);     // newly added
    std::string customerName_;
};
#include "shop.h"
#include "iostream"
using namespace std;

void RealShop::provideService(string product) {
    cout<<"    shop is building "<<product<<" in warehouse"<<endl;
    if (specialOrder_ == true){
        specialService("Minh");
    }
    collectMaterial();
}

void RealShop::collectMaterial() {
    cout<<"    collect material to build product"<<endl;
}

RealShop::RealShop(){
    cout<<"    RealShop constructor"<<endl;
}

RealShop::~RealShop(){
    cout<<"    RealShop destructor"<<endl;
}

void RealShop::specialService(string customer){
    std::cout<<"provide custom order to customer "<<customer<<std::endl;
    customerName_ = customer;
}

The following shows the log printed out when we apply the changes to RealShop class. We only rebuild the shop.cpp with the command g++ -c shop.cpp. Then we linked the already available object files to make executable files. The executable runs and output new changes.

  • The following output shows the effect of the changes:
C:\> g++ -c *.cpp          --> this command creates object files
C:\> g++ -o main *.o       --> this command links object files to exe file
C:\> .\main.exe
**ShopProxy Constructor**
Proxy creates RealShop on request
    RealShop constructor
    shop is building a doll in warehouse
provide custom order to customer Minh         --> changes are updated
    collect material to build product
Proxy can provide service
    shop is building a doll in warehouse
provide custom order to customer Minh         --> changes are updated
    collect material to build product
**ShopProxy Destructor**
    RealShop destructor
**ShopProxy Constructor**
Proxy creates RealShop on request
    RealShop constructor
    shop is building a doll in warehouse
provide custom order to customer Minh         --> changes are updated
    collect material to build product

Using function pointer in proxy

In the following program, I define:

  • a function pointer provideService_proxy as member of ShopProxy
void (RealShop::* provideService_proxy )(std::string) = nullptr ;
  • in order to assign the address of RealShop::provideService to my function pointer provideService_proxy, I have to do the following:
provideService_proxy = &RealShop::provideService;
(realShop_->*provideService_proxy)(product);
  • in order to call RealShop::provideService to perfom work, I dont have to call it via instance of RealShop and make another function, add values to callstack, I can just call it via the proxy. The proxy will go to the address of RealShop::provideService, that was saved to provideService_proxy.
(realShop_->*provideService_proxy)(product);

Code:

#include "iostream"
#include "string"
using namespace std;

// needed preprocessor to prevend loop include
#ifndef ISHOP
#define ISHOP
#include "string"

class IShop{
    public:
    IShop(){};
    virtual void provideService(std::string product) = 0;
    virtual ~IShop(){};
};
#endif

class RealShop: public IShop {
    public:
    RealShop(){
        cout<<"RealShop constructor"<<endl;
    };
    virtual ~RealShop(){
        cout<<"RealShop destructor"<<endl;
    };
    virtual void provideService(std::string product) override{
       cout<<"    shop is building "<<product<<endl;
    } ;
};

class ShopProxy : public IShop {
    public:
    ShopProxy(){
        cout<<"**ShopProxy constructor**"<<endl;

    };
    virtual ~ShopProxy(){ 
        cout<<"**ShopProxy destructor**"<<endl;
        delete realShop_;
    };
    virtual void provideService(std::string product) override {
        if ( realShop_ == nullptr  ) {                  
                
            //create RealShop, call RealShop to do service
            // provideService_proxy = (void (*)(std::string))init_RealService (product);            
            init_RealService (product);
        } 
        else{
            std::cout<<"Proxy can provide service"<<std::endl;
            // RealShop is available in Proxy's memory.
            // don't have to request another function call, and add to stack
            // save stack memory by using function pointer. 
            (realShop_->*provideService_proxy)(product);                 
        }
    };
    
    private:
    // typedef void (* functPtr )(std::string product);
    // void (* provideService_proxy )(std::string product) = nullptr;
    void (RealShop::* provideService_proxy )(std::string) = nullptr ;
    void init_RealService ( std::string product ){
        std::cout<<"Proxy creates RealShop on request"<<std::endl; 
        realShop_ = new RealShop();
        std::cout<<"init_RealService saves realShop_->provideService to function pointer"<<endl; 
        provideService_proxy = &RealShop::provideService;          
        (realShop_->*provideService_proxy)(product);
    };

    RealShop * realShop_ = nullptr;    // safe initialization of pointer
};

int main()
{
    std::string requestProduct1 = "REQUEST 1: a doll";
    IShop * myShopProxy = new ShopProxy();
    // myShopProxy creates RealShop
    myShopProxy->provideService(requestProduct1);
    // myShopProxy already has RealShop, and can provide service.
    std::string requestProduct2 = "REQUEST 2: a toy";
    myShopProxy->provideService(requestProduct2);

    // delete myShopProxy will delete RealShop and data saved in memory
    delete myShopProxy;

    // create new proxy
    std::string requestProduct3 = "REQUEST 3: a fan";
    IShop * myShopProxy2 = new ShopProxy();
    // myShopProxy has to create new RealShop again and reload data.
    myShopProxy2->provideService(requestProduct3);

    delete myShopProxy2;
}

Output:

The following outputs explain that:

  • RealShop is instantiated only once when Proxy is first requested to run provideService.
  • In the first time, the function RealService::provideService is called. But in the second time, the proxy's function pointer provideService_proxy does the job. It does the job by going back to the address of the origin RealService::provideService.
**ShopProxy constructor**
Proxy creates RealShop on request
RealShop constructor
init_RealService saves realShop_->provideService to function pointer
    shop is building REQUEST 1: a doll
Proxy can provide service
    shop is building REQUEST 2: a toy
**ShopProxy destructor**
RealShop destructor
**ShopProxy constructor**
Proxy creates RealShop on request
RealShop constructor
init_RealService saves realShop_->provideService to function pointer
    shop is building REQUEST 3: a fan
**ShopProxy destructor**
RealShop destructor

Leave a Reply

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