2

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++?

dfu
  • 51
  • 4
  • Does it need to only be immutable from outside code, or do you not want your object to be able to modify it either? –  Sep 09 '19 at 20:50
  • 3
    You might want to research keyword `mutable`. – SergeyA Sep 09 '19 at 20:56
  • 2
    btw, the *dirty hack* you mentioned is UB. – Walter Sep 09 '19 at 21:01
  • 2
    This just *feels* wrong. Whatever way you'd solve it, forcing launguage into a design pattern is going to be confusing for the readers. I'd either make the class really immutable and delay initialization at call site (e.g. a storage class which keeps the instances of the class and creates new ones only when needed) or wouldn't pretend that it's immutable and just admit it's a cache and it changes once in it's lifetime. – Yksisarvinen Sep 09 '19 at 21:03
  • Your casts are a weird way to do `const_cast`, in a place where you shouldn't be doing `const_cast`. – Lightness Races in Orbit Sep 09 '19 at 21:46
  • Looks like you're making everything `const` "for the sake of it" i.e. so you can call the entire class immutable. Instead why not make `const` only what should be `const`? Then the question is moot. – Lightness Races in Orbit Sep 09 '19 at 21:47
  • Why do you have one-element arrays? – Lightness Races in Orbit Sep 09 '19 at 21:49

1 Answers1

3

This is a red herring: since the cache is private, it doesn't matter whether it's const or not: you (the designer of the class's member functions) have complete control over any attempt to alter its value.

Here is a C++ implementation, where the value is accessed by a const member

template<typename valueType>
struct cachedFunctionValue {
    cachedFunctionValue(std::function<valueType()> &&f)
    : func(f) {}

    valueType get() const       // constant access to function value
    {
        if(!cache.has_value())
            cache = func();     // works because cache is mutable
        return cache.value();
    }
private:
    const std::function<valueType()> func;
    mutable std::optional<valueType> cache;
};
Walter
  • 44,150
  • 20
  • 113
  • 196