【发布时间】:2019-09-09 20:37:33
【问题描述】:
不久前我已经了解了“优雅对象”原则(参见优雅对象.org),它们很容易在 C# 中遵循,但现在我正在做一些 c++,不变性给我带来了一些麻烦。我正在寻找有关如何以不可变的方式实现延迟初始化和缓存的最佳实践。
惰性初始化和缓存的结合意味着一些数据必须在对象构造后存储在对象内部,这需要一些可变的行为。 在 C# 中,如果类的所有字段都声明为“只读”,则您的对象是不可变的。幸运的是,这是一种浅层不变性,因此您的不可变对象可以将可变对象封装为只读字段并对其进行操作以实现可变行为,请参见下面的 C# 示例。
最接近 C# 在 C++ 中的“只读”的做法是将字段声明为“常量”。但是,在 C++ 中,如果将其他可变对象存储在 const 字段中,则不能像在 C# 中那样直接操作它。尝试在 C++ 中重新创建我的 C# 示例会导致编译时错误,请参见下面的第二个示例。
有一个解决方法:将每个数组转换为 void 指针并返回到我需要的类型的指针(例如 int 或 bool),然后将该指针视为数组,这样我就可以绕过原始数组的 const 限定.虽然这很丑陋,但它似乎是一些肮脏的 hack,并且使代码的可读性低于我刚刚删除 const 限定符时的可读性。
我真的很想要那些 const 限定符,它们是向人们保证该类确实是不可变的一种正式方式,而无需阅读整个类的代码。
我想要实现的 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]'
这个错误不是问题,我知道为什么会抛出它,我只是为了完整性而添加它。
总而言之,我希望一个类进行延迟初始化并缓存结果,但我也希望该类是不可变的。在 c++ 中最优雅的方法是什么?
【问题讨论】:
-
它是否只需要在外部代码中是不可变的,还是您不希望您的对象也能够修改它?
-
您可能想研究关键字
mutable。 -
顺便说一句,你提到的 dirty hack 是 UB。
-
这只是感觉错了。无论你用什么方法来解决它,强迫语言进入设计模式都会让读者感到困惑。我要么使该类真正不可变并在调用站点延迟初始化(例如,一个存储类,它保留类的实例并仅在需要时创建新实例),或者不会假装它是不可变的,只是承认它是一个缓存和它在其生命周期中改变一次。
-
你的演员是一种奇怪的方式来做
const_cast,在一个你不应该做const_cast的地方。
标签: c++ oop design-patterns immutability lazy-initialization