通过间接示例解释 C++ 可变性

Explain C++ Mutability through Indirection example

在 C++ 编程语言第 4 版的第 16.2.9.4 节“间接可变性”中有一个使用间接而不是 mutable 关键字进行惰性求值的例子的草图。

struct cache {
    bool valid;
    string rep;
};
 
class Date {
public:
    // ...
    string string_rep() const;
private:
    cache * c;    // initialize in constructor
    void compute_cache_value() const;
    // ...
};
 
string Date::string_rep() const {
    if (!c->valid) {
        compute_cache_value();
        c->valid = true;
    }
    return c->rep;
}

Full runnable example.

解释不多:

Declaring a member mutable is most appropriate when only a small part of a representation of small object is allowed to change. More complicated cases are often better handled by placing the changing data in a separate object and accessing it directly.

我正在寻找更完整的解释。特别是,

换句话说,这是一个真实的模式还是一个人为的例子来证明间接性与常量性?

"smallness constraint" 并不是真正的约束,只是提示如何编写代码(与使用的内存没有太大关系)。如果你有一个包含 30 个成员的 class,其中 20 个是可变的,那么将它分成两个 class 可能更有意义(一个用于可变部分,一个用于其余部分)。

为什么它不是智能指针:不知道,但可能是书作者太累了:p

为什么它是一个指针:你是对的,没有必要。制作一个没有任何指针的可变缓存对象也可以,如果不需要任何类似指针的东西(比如从外部获取现有对象),指针只会增加另一种制造错误的可能性。

警告:以下文字对战场程序员没有任何实用价值,而不是对程序员-哲学家或普通哲学家。另外,我是 Bjarne Stroustrup 的忠实粉丝,我的观点可能有偏见。不幸的是,Whosebug 格式不适合书籍讨论。

此外,我们正在讨论关于常量可变性的棘手问题,我们应该对编译器和 class 用户撒谎。并且没有单一的正确意见。我已准备好接受评论和投票 ;)

简而言之:

  • 你可能没有理解惰性初始化的确切含义(可能是因为书中没有正确选择术语)与 RAII 一样,我们知道 Bjarne 不擅长选择术语 ;)
  • 在写一本关于编程的书时,需要做出的决定很少。所以有些问题归结为 "How to write a book?" 而不是 "How to write production code?".

长:

  1. What is the smallness constraint? Is it a small amount of memory or a small amount of logic?

    我将再次引用 Bjarne:

    Declaring a member mutable is most appropriate when only a small part of a representation of small object is allowed to change

    我认为他在这里的意思是 "small number of data members"。通过将数据成员分组到单独的 class 中进行重构通常是一个很好的建议。 "appropriate" 和 "small" 之间的比率是多少?你自己决定(给定一个真正的问题,一个分析器工具和 memory/speed/battery_life/money/client_happiness 等的约束)

  2. Doesn't initializing c in the constructor defeat (to a nontrivial degree) the laziness? That is, it does work you many never need.

    好吧,延迟初始化是指每次用户请求字符串时 非计算 正确的字符串值(即 compute_cache_value()),但仅在真正需要时。不是用空字符串初始化,对吧? (std::string 在构造时初始化为空字符串)

    16.2.9.3 和 16.2.9.4 章 Bjarne 的代码中没有任何构造函数!而且您也不会在代码的构造函数中计算字符串,而是使用空字符串文字对其进行初始化。所有计算都延迟到最后一刻。所以,惰性初始化在这里非常适合我。

    作为进一步的过早优化,如果你想要真正的惰性初始化,你可能会离开cache* 指针在构造函数中未初始化,并在第一次 Date::string_rep() 调用时分配。如果您的缓存很大并且用户从不需要它,这将是一堆安全的堆。通过这种方式,您可以将计算包装在 cache 构造函数中,从而将惰性评估呈现为真正的惰性初始化

  3. Why is c a naked pointer instead of something like unique_ptr? The previous chapters went to a bit of effort to demonstrate exception safety and RAII.

    在"C++ Programming Language, 4th edition"第17章介绍了智能指针,我们正在谈论第16章。另外,描述可变性并不重要,只要你设法[就不会带来任何好处delete 在析构函数中。另一件事是作者会在本章中解释为什么你可以改变 smart_ptr cache 拥有的资源,在 const 方法中只有常量 smart_ptr 对象,这将引入描述运算符重载 (大多数高级 Java 和 Python 程序员会在那个地方扔掉这本书 ;)).

    除此之外,这通常是一个难题。首先,Bjarne Stroustrup 的书籍主要被视为教材或指南。那么,在教新人的时候,我们应该先跳到智能指针还是先教原始指针呢?我们应该立即使用标准库还是留到最后几章再使用? C++14 从头开始​​还是从 "C+" 开始?谁知道?还有一个称为 "over-usage of smart pointers" 的问题,特别是 shared_ptr.

  4. Why not just have a mutable cache c member if you're going to allocate and initialize c in the constructor anyway?

    16.2.9.3中描述的就是这样吧?

    并在此处添加一个间接级别是替代解决方案(显示 "there is no universal solutions for all purposes")和这个惊人引用的演示:

    All problems in computer science can be solved by another level of indirection, except for the problem of too many layers of indirection. – David J. Wheeler

    没有。正如用户 xan 所阐明的那样,16.9.3 是关于多个可变成员的,而单个 mutable struct 将提供一些关注点分离。

希望您喜欢阅读!