【问题标题】:Immutable "functional" data structure in C++11C++11 中不可变的“函数式”数据结构
【发布时间】:2023-04-02 04:25:01
【问题描述】:

我试图为我对多线程/并发场景感兴趣的几个数据结构编写一些实现。

很多函数式语言,几乎我所知道的所有,都以不可变的方式设计自己的数据结构,所以这意味着如果你要将value添加到实例t1 T,你真的得到了一个新的 T 实例,它包含 t1 + value

 container t;
 container s = t; //t and s refer to the same container.
 t.add(value); //this makes a copy of t, and t is the copy

我在 C++11 中找不到合适的关键字来执行此操作;标准库中的关键字、语义和函数显然是面向函数式方法的,特别是我发现:

  • mutable 它不是用于运行时,它更有可能是对编译器的提示,但是这个关键字并不能真正帮助您设计新的数据结构或以 不可变 方式使用数据结构
  • swap 不适用于临时工,这对我来说是一个很大的缺点

我也不知道其他关键字/函数对这样的设计有多大帮助,swap 是其中一个非常接近好的东西,所以我至少可以开始写一些东西,但显然它仅限于lvalues .

所以我要问:是否可以在 C++11 中使用函数式方法设计 不可变 数据结构?

【问题讨论】:

  • 当然这是可能的。是什么让您认为它不是,更具体地说,它对您来说似乎很难/不可能?
  • 您能做的最好的事情就是始终使用const 限定符声明数据结构的instances。问题是,与真正的函数式语言(例如 Haskel)相比,您不会获得那么多。这样的语言高度利用所有值都是常量的事实,并且每个表达式只依赖于它自己(而不依赖于其他状态)。无论数据结构的设计如何,C++ 都不是这种情况。您可能想要告诉编译器只允许您的类的 const 实例,这是不可能的。
  • 设计一个不可变的数据结构很容易......只需创建一个包含数据成员的容器,例如现有的标准库数据结构之一,然后让变异函数进行深度复制,变异副本,然后按值返回。您可以从任何类型的代码中使用此类对象。不是特别可扩展,但完全是挑你的毒药。
  • @user2485710: const std::vector<T> 是我们标准的不可变数据结构。

标签: c++ multithreading c++11 data-structures immutability


【解决方案1】:

您只需声明一个带有私有成员变量的类,并且不提供任何方法来更改这些私有成员的值。就是这样。您仅从类的构造函数初始化成员。没有人能够以这种方式更改类的数据。 C++ 创建不可变对象的工具是成员的私有可见性。

mutable:这是 C++ 中最大的 hack 之一。在我的一生中,我最多见过 2 个地方使用它是合理的,而且这个关键字与您正在搜索的内容几乎相反。如果您要在 C++ 中搜索可帮助您在编译时标记数据成员的关键字,那么您正在搜索 const 关键字。如果将类成员标记为 const,则只能从构造函数的 INITIALIZER LIST 对其进行初始化,并且在实例的整个生命周期内都不能再修改它们。这不是 C++11,它是纯 C++。没有神奇的语言特性可以提供不变性,你只能通过巧妙地编程来做到这一点。

【讨论】:

  • 我偶尔使用mutable 来延迟加载成员。
  • 可变的很适合懒惰的评估
  • @user2485710,问一个没有代码的模糊问题,得到一个没有代码的模糊答案。如果您需要更具体的建议,您的问题需要更具体。
  • 我认为你有一个精确的方法来创建一个不可变的类,你不需要神奇的 C++11 关键字。 @leemes 我可以回忆起可变的两个合理示例: const/不可变对象的侵入式引用计数,也许还有一些调试计数器/变量...在大多数情况下,对象可能不是真正的 const 或其可变/非 const 部分不应该t 是一个不可变的成员,而是另一个被引用的对象。
  • @user2485710: 除了const
【解决方案2】:

c++ 中,“不变性”由 const 关键字授予。当然 - 您仍然可以更改 const 变量,但您必须故意这样做(例如 here)。在正常情况下,编译器不会让你这样做。由于您最关心的似乎是以 功能 样式进行操作,并且您想要一个 结构,因此您可以自己定义它:

class Immutable{
   Immutable& operator=(const Immutable& b){} // This is private, so it can't be called from outside
   const int myHiddenValue;
public:
   operator const int(){return myHiddenValue;}
   Immutable(int valueGivenUponCreation): myHiddenValue(valueGivenUponCreation){}
};

如果你定义一个这样的类,即使你尝试将myHiddenValue 更改为const_cast,它实际上也不会做任何事情,因为在调用operator const int 期间会复制该值。

注意:没有真正的理由这样做,但是嘿 - 这是你的愿望。

另请注意:由于 C++ 中存在 指针,您仍然可以使用某种指针魔术来更改值(获取对象的地址、计算偏移量等),但您可以t 真的有帮助。即使使用函数式语言,如果它有指针,你也无法防止这种情况发生。

另外,您为什么要强迫自己以函数式方式使用 C++?我可以理解它对您来说更简单,并且您已经习惯了它,但是由于它的缺点,函数式编程并不经常使用。请注意,无论何时创建新对象,都必须分配空间。对于最终用户而言,它速度较慢

【讨论】:

  • 因为当涉及到并发性时,不可变的数据结构真的可以帮助你,而且,最大的好处是一切都是按设计发生的,所以你比只使用一堆互斥锁或锁或尝试更适合做一些花哨的事情。
  • 如果您不是一开始就尝试更改它们,那么有什么区别?您的问题的答案是:只需编写一个接受 X 类型参数并返回 X 类型的函数。
  • 这真的不是不可变数据结构的用途,假设您将t 作为T 的一个实例,您可以随意更改t,魔法就在设计中的Tbartoszmilewski.com/2013/11/13/… 的第一行
  • @user2485710 你错了。神奇之处在于您调用T 的函数,而不是T 本身。函数式语言没有什么神奇之处。您只需要 const T f(const T t) 类型的函数,它们采用“旧”t 并返回一个“新”,更改了一个,而不触及第一个。这就是我在学习 SML 时被教导的方式。
  • @user2485710 也许我刚刚学错了函数式语言(主要是 scheme/clos),但至少在我见过的每一种函数式语言中,如果你真的想要的话,你仍然可以改变状态。这意味着如果你不关心你的操作,你也可以用函数式语言编写可变数据结构。无论如何,不​​可变的数据结构在 C++ 中非常有用,大家也同意——在没有可变数据结构的情况下,并发已经足够困难了。
【解决方案3】:

Bartoz Milewski has implemented Okasaki's functional data structures in C++。他对why functional data structures are important for concurrency 进行了非常详尽的论述。在那篇论文中,他解释了并发构造对象然后使其不可变的必要性:

下面是需要发生的事情:一个线程必须以某种方式构造 它注定是不可变的数据。根据结构 这些数据,这可能是一个非常简单或非常复杂的过程。然后 该数据的状态必须被冻结——不再有任何变化 允许。

正如其他人所说,当您想在 C++ 中公开数据并且无法对其进行更改时,您可以使您的函数签名如下所示:

class MutableButExposesImmutably
{
    private:
        std::string member;
    public:
        void complicatedProcess() { member = "something else"; } // mutates
        const std::string & immutableAccessToMember() const {
            return member;
        }
};

这是一个可变数据结构的例子,但你不能直接改变它。

我认为您正在寻找类似于 java 的 final 关键字:此关键字允许您构造一个对象,但此后该对象保持不可变。

您可以在 C++ 中执行此操作。以下代码示例编译。请注意,在 Immutable 类中,对象 member 是字面上不可变的(与前面的示例不同):您可以构造它,但一旦构造,它就是不可变的。

#include <iostream>
#include <string>
using namespace std;

class Immutable
{
    private:
        const std::string member;
    public:
        Immutable(std::string a) : member(a) {}
        const std::string & immutable_member_view() const { return member; }
};


int main() {
    Immutable foo("bar");
    // your code goes here
    return 0;
}

【讨论】:

  • Bartoz Milewski 指出 C++不是对于持久数据结构实现来说是一种好的语言,它通常被认为是任何好的函数式语言的先决条件。特别是,Bartoz Milewski 说,缺乏适当的尾调用优化或垃圾收集使得任何冈崎思想的 C++ 实现(在 Clojure 和其他语言中使用)在 C++ 中都非常脆弱。 Clojure 的创建者 Rich Hickey 也是 C++ 大师,他的观点是,适当的不可变函数结构不是 C++ 擅长的。
  • 感谢@johnbakers 的输入!我正在尝试在 C++ 中实现需要这些类型的结构。我可能做错的反馈很有价值:)
  • 这不仅仅是你做错了,而是你真的不能在 C++ 中做到这一点,就像在适当的函数式语言中通常所期望的那样。请参阅此处了解更多信息:github.com/BartoszMilewski/Okasaki/issues/1 在我看来,不应该试图强迫一种语言做它不擅长的事情。以设计方式使用 C++ 的所有奇妙优势,并根据这些习惯用法重新思考您的 C++ 代码,或者使用不同的语言。
  • Java 的 final 不会使对象不可变。它使引用不可变。就像int * const,而不是int const *。这是一个很大的不同。
【解决方案4】:

回复。 st 的代码示例。您可以在 C++ 中执行此操作,但如果我正确理解您的要求,“不变性”与该问题无关!

我在供应商库中使用了容器,它们确实按照您描述的方式运行;即,当它们被复制时,它们会共享其内部数据,并且在需要更改其中一个之前,它们不会复制内部数据。

请注意,在您的代码示例中,如果s 发生更改,则t 不得更改。所以s 必须包含某种标志或引用计数,以表明t 当前正在共享其数据,因此当s 的数据发生更改时,它需要拆分副本而不是仅更新其数据。

因此,作为容器外观的一个非常宽泛的轮廓:它将由一个指向某些数据的句柄(例如一个指针)以及一个引用计数组成;并且您更新数据的函数都需要检查 refcount 以决定是否重新分配数据;并且您的复制构造函数和复制分配运算符需要增加引用计数。

【讨论】:

  • 幸运的是,复杂的部分已经完成了! std::shared_ptr
猜你喜欢
  • 2012-01-20
  • 2014-06-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-01-29
相关资源
最近更新 更多