如何在不可变的 C++ 对象中实现惰性初始化和缓存?

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

不久前我了解了 'Elegant Objects' 原则(参见 elegantobjects.org),它们很容易在 C# 中遵循,但现在我正在做一些 C++,不变性是给我带来一些麻烦。我正在寻找有关如何以不可变方式实现惰性初始化和缓存的最佳实践。

惰性初始化和缓存的组合意味着一些数据必须在对象构造后存储在对象内部,这需要一些可变的行为。 在 C# 中,如果 class 的所有字段都声明为 'readonly',则您的对象是不可变的。幸运的是,这是一种浅层不变性,因此您的不可变对象可以将可变对象封装为只读字段并对其进行操作以实现可变行为,请参阅下面的 C# 示例。

在 C++ 中最接近 C# 的 'readonly' 的是将字段声明为 'const'。但是,在 C++ 中,如果您在 const 字段中存储一个可变对象,则不能像在 C# 中那样直接操作它。尝试在 C++ 中重新创建我的 C# 示例会导致编译时错误,请参阅下面的第二个示例。

有一个解决方法:将每个数组转换为空指针并返回到我需要的类型的指针(例如 int 或 bool),然后将该指针视为数组允许我绕过 const 限定原始数组。虽然这很丑陋,但它看起来像是一些肮脏的 hack,并且使代码的可读性不如我刚刚删除 const 限定符时的可读性。

不过,我确实想要那些 const 限定符,它们是一种正式的方式,可以让人们确信 class 确实是不可变的,而无需他们通读整个 class 的代码。

我正在努力实现的 C# 示例:

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++示例:

#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();
}

丑陋的解决方法:

#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();
}

尝试编译第二个示例时抛出的错误:

 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]'

这个错误不是问题,我知道为什么会抛出它,我只是为了完整性添加它。

综上所述,我希望 class 进行惰性初始化并缓存结果,但我也希望 class 是不可变的。在 C++ 中最优雅的方法是什么?

这是一个转移注意力的问题:因为缓存是 private,所以它是否是 const 并不重要:您(class 成员的设计者函数)可以完全控制任何更改其值的尝试。

这是一个 C++ 实现,其中值由 const 成员访问

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;
};