lundi 9 septembre 2019

How do i implement lazy initialization and caching in an immutable c++ object?

I've learned about the 'Elegant Objects' principles (see elegantobjects.org) a while back and they are easy enough to follow in C#, but now that i'm doing some c++ the immutability is causing me some trouble. I'm looking for best practices on how to implement lazy initialization and caching in an immutable way.

The combination of lazy initialization and caching means that some data will have to be stored inside the object after its construction, which requires somewhat mutable behavior. In C# your objects are immutable if all fields of the class are declared as 'readonly'. Fortunately, this is a shallow immutability, so your immutable object can encapsulate a mutable object as a readonly field and manipulate that to achieve mutable behavior, see the C# sample below.

The closest thing to C#'s 'readonly' in C++ would be to declare the fields as 'const'. However, in C++, if you store an otherwise mutable object in a const field, you can't directly manipulate it the same way you could in C#. Trying to recreate my C# example in C++ results in compile time errors for me, see the second example below.

There is a workaround: Casting each array into a void pointer and back to a pointer of the type i need (e.g. int or bool) and then treating that pointer as an array allows me to bypass the const qualification of the original array. This is ugly though, it seems like some dirty hack and makes the code way less readable than it would be if i just removed the const qualifier.

I really want those const qualifiers there though, they are a formal way of assuring people that the class really is immutable, without them having to read through the entire class's code.

C# example of what i'm trying to achieve:

using System;

public sealed class Program
{
    public static void Main()
    {
        var test = 
            new CachedInt(
                ()=>5 // this lambda might aswell be a more expensive calculation, which would justify lazy initialization and caching
            );
        Console.WriteLine(test.Value());
    }
}

public sealed class CachedInt
{
    //note that these are all readonly, this is an immutable class
    private readonly Func<int> source;
    private readonly int[] cache;
    private readonly bool[] hasCached;

    public CachedInt(Func<int> source)
    {
        this.source = source;
        this.cache = new int[1];
        this.hasCached = new bool[1]{false};
    }

    public int Value()
    {
        if(!this.hasCached[0])
        {
            // manipulating mutable objects stored as readonly fields:
            this.cache[0] = this.source.Invoke();
            this.hasCached[0] = true;
        }
        return this.cache[0];
    }
}

C++ example that causes compile time errors:

#include <iostream>
#include <functional>

class CachedInt final
{
private:

    // all const, this is an immutable class
    const std::function<int()> source;
    const int cache[1];
    const bool hasCached[1];

public:

    CachedInt(std::function<int()> source) :
        source(source),
        cache{0},
        hasCached{false}
    {}

    int Value()
    {
        if(!this->hasCached[0])
        {
            // the following two lines obviously don't work due to the const qualification
            this->cache[0] = this->source();
            this->hasCached[0] = true;
        }
        return this->cache[0];
    }
};

int main()
{
    CachedInt test([]()->int{return 5;});
    std::cout << test.Value();
}

Ugly workaround:

#include <iostream>
#include <functional>

class CachedInt final
{
private:

    // all const, this is an immutable class
    const std::function<int()> source;
    const int cache[1];
    const bool hasCached[1];

public:

    CachedInt(std::function<int()> source) :
        source(source),
        cache{0},
        hasCached{false}
    {}

    int Value()
    {
        if(!this->hasCached[0])
        {
            // this works but it's ugly. there has to be a better way.
            ((int*)(void*)this->cache)[0] = this->source();
            ((bool*)(void*)this->hasCached)[0] = true;
        }
        return this->cache[0];
    }
};

int main()
{
    CachedInt test([]()->int{return 5;});
    std::cout << test.Value();
}

The error thrown when trying to compile the second sample:

 In member function 'int CachedInt::Value()':
24:28: error: assignment of read-only location '((CachedInt*)this)->CachedInt::cache[0]'
25:32: error: assignment of read-only location '((CachedInt*)this)->CachedInt::hasCached[0]'

This error is not the problem, i know why it is thrown, i'm just adding it for completeness.

To sum up, i want a class to do lazy initialization and cache the result, but i also want the class to be immutable. What is the most elegant way of doing this in c++?

Aucun commentaire:

Enregistrer un commentaire