在C ++中的懒惰评估

Lazy evaluation in C++

C++没有对惰性评估的原生支持(如Haskell所做的那样)。

我想知道是否可以以合理的方式在C++中实现惰性评估。如果是,你会怎么做?

编辑:我喜欢康拉德·鲁道夫的回答。

我想知道是否有可能以一种更通用的方式实现它,例如使用一个参数化的类lazy来实现它,它基本上是按照matrix的方式为t工作的。

在t上的任何操作都将返回lazy。唯一的问题是将参数和操作代码存储在lazy本身中。有人知道如何改进这个吗?


I'm wondering if it is possible to implement lazy evaluation in C++ in a reasonable manner. If yes, how would you do it?

是的,这是可能的,而且经常这样做,例如用于矩阵计算。实现这一点的主要机制是操作符重载。考虑矩阵加法的情况。函数的签名通常如下所示:

1
matrix operator +(matrix const& a, matrix const& b);

现在,为了让这个函数变得懒惰,只需返回一个代理,而不返回实际的结果:

1
2
3
4
5
struct matrix_add;

matrix_add operator +(matrix const& a, matrix const& b) {
    return matrix_add(a, b);
}

现在需要做的就是编写这个代理:

1
2
3
4
5
6
7
8
9
10
11
struct matrix_add {
    matrix_add(matrix const& a, matrix const& b) : a(a), b(b) { }

    operator matrix() const {
        matrix result;
        // Do the addition.
        return result;
    }
private:
    matrix const& a, b;
};

神奇之处在于方法operator matrix(),它是从matrix_add到普通matrix的隐式转换运算符。这样,您就可以链接多个操作(当然,通过提供适当的重载)。只有将最终结果分配给matrix实例时,才会进行评估。

编辑我应该更明确些。事实上,代码没有任何意义,因为尽管计算发生得很慢,但它仍然发生在同一个表达式中。特别是,另一个加法将评估此代码,除非将matrix_add结构更改为允许链式加法。通过允许可变模板(即可变长度的模板列表),C++ 0x极大地促进了这一点。

但是,在一个非常简单的情况下,此代码实际上会有一个真正的、直接的好处,如下所示:

1
int value = (A + B)(2, 3);

这里,假设AB是二维矩阵,用fortran表示法进行解引用,即上述方法从矩阵和中计算出一个元素。当然,添加整个矩阵是浪费的。matrix_add援救:

1
2
3
4
5
6
7
8
struct matrix_add {
    // … yadda, yadda, yadda …

    int operator ()(unsigned int x, unsigned int y) {
        // Calculate *just one* element:
        return a(x, y) + b(x, y);
    }
};

其他例子不胜枚举。我只记得不久前我已经实现了一些相关的东西。基本上,我必须实现一个字符串类,它应该遵循一个固定的、预定义的接口。然而,我的特殊字符串类处理的是实际上没有存储在内存中的巨大字符串。通常,用户只需使用函数infix从原始字符串访问小的子字符串。我为字符串类型重载了这个函数,以返回一个代理,该代理包含对字符串的引用,以及所需的开始和结束位置。只有当实际使用此子字符串时,它才查询C API来检索字符串的这一部分。


助推。lambda很不错,但是助推。Proto正是你想要的。它已经具有所有C++运算符的重载,默认情况下,当调用EDCOX1×2调用时,它们可以执行它们通常的函数,但是可以被改变。


Konrad已经解释的内容可以进一步支持嵌套调用操作符,所有这些都是延迟执行的。在Konrad的例子中,他有一个表达式对象,可以为一个操作的两个操作数存储两个参数。问题是,它只会延迟地执行一个子表达式,这很好地解释了延迟评估中的概念,用简单的术语来说,但并不能显著提高性能。另一个例子也很好地展示了如何应用operator()只添加一些使用该表达式对象的元素。但要计算任意复杂的表达式,我们也需要某种机制来存储它的结构。我们不能绕过模板来做这件事。名字叫以东十一〔一〕。其思想是,一个模板化的表达式对象可以递归地存储某些任意子表达式的结构,例如树,其中操作是节点,操作数是子节点。为了得到一个很好的解释,我今天才发现(在我写下下面的代码后的几天),请看这里。

1
2
3
4
5
6
7
8
9
10
11
12
template<typename Lhs, typename Rhs>
struct AddOp {
    Lhs const& lhs;
    Rhs const& rhs;

    AddOp(Lhs const& lhs, Rhs const& rhs):lhs(lhs), rhs(rhs) {
        // empty body
    }

    Lhs const& get_lhs() const { return lhs; }
    Rhs const& get_rhs() const { return rhs; }
};

它将存储任何添加操作,甚至嵌套操作,如以下简单点类型的运算符+定义所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct Point { int x, y; };

// add expression template with point at the right
template<typename Lhs, typename Rhs> AddOp<AddOp<Lhs, Rhs>, Point>
operator+(AddOp<Lhs, Rhs> const& lhs, Point const& p) {
    return AddOp<AddOp<Lhs, Rhs>, Point>(lhs, p);
}

// add expression template with point at the left
template<typename Lhs, typename Rhs> AddOp< Point, AddOp<Lhs, Rhs> >
operator+(Point const& p, AddOp<Lhs, Rhs> const& rhs) {
    return AddOp< Point, AddOp<Lhs, Rhs> >(p, rhs);
}

// add two points, yield a expression template    
AddOp< Point, Point >
operator+(Point const& lhs, Point const& rhs) {
    return AddOp<Point, Point>(lhs, rhs);
}

现在,如果你有

1
2
Point p1 = { 1, 2 }, p2 = { 3, 4 }, p3 = { 5, 6 };
p1 + (p2 + p3); // returns AddOp< Point, AddOp<Point, Point> >

现在只需要重载operator=并为点类型添加适当的构造函数,然后接受addop。将其定义更改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct Point {
    int x, y;

    Point(int x = 0, int y = 0):x(x), y(y) { }

    template<typename Lhs, typename Rhs>
    Point(AddOp<Lhs, Rhs> const& op) {
        x = op.get_x();
        y = op.get_y();
    }

    template<typename Lhs, typename Rhs>
    Point& operator=(AddOp<Lhs, Rhs> const& op) {
        x = op.get_x();
        y = op.get_y();
        return *this;
    }

    int get_x() const { return x; }
    int get_y() const { return y; }
};

并将适当的get_x和get_y作为成员函数添加到addop中:

1
2
3
4
5
6
7
int get_x() const {
    return lhs.get_x() + rhs.get_x();
}

int get_y() const {
    return lhs.get_y() + rhs.get_y();
}

请注意,我们没有创建任何Point类型的临时文件。它可能是一个包含许多字段的大矩阵。但是在需要结果的时候,我们计算得很慢。


我没有什么可以添加到Konrad的帖子中,但是你可以在真实世界的应用程序中查看eigen,以获得一个懒惰的正确评估的例子。这是相当令人敬畏的。


约翰内斯的回答是有效的,但当涉及到更多的括号时,它并没有按预期工作。下面是一个例子。

1
2
Point p1 = { 1, 2 }, p2 = { 3, 4 }, p3 = { 5, 6 }, p4 = { 7, 8 };
(p1 + p2) + (p3+p4)// it works ,but not lazy enough

因为三个超载的+操作员没有覆盖箱子

1
AddOp<Llhs,Lrhs>+AddOp<Rlhs,Rrhs>

所以编译器必须将(p1+p2)或(p3+p4)转换为点,这还不够懒,当编译器决定要转换哪个时,它会抱怨。因为没有一个比另一个更好。下面是我的扩展名:添加另一个重载的运算符+

1
2
3
4
5
6
    template <typename LLhs, typename LRhs, typename RLhs, typename RRhs>
AddOp<AddOp<LLhs, LRhs>, AddOp<RLhs, RRhs>> operator+(const AddOp<LLhs, LRhs> & leftOperandconst, const AddOp<RLhs, RRhs> & rightOperand)
{
    return  AddOp<AddOp<LLhs, LRhs>, AddOp<RLhs, RRhs>>(leftOperandconst, rightOperand);

}

现在,编译器可以正确地处理上面的情况,并且没有隐式转换,volia!


我正在考虑实现一个使用std::function的模板类。该类或多或少应如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
template <typename Value>
class Lazy
{
public:
    Lazy(std::function<Value()> function) : _function(function), _evaluated(false) {}

    Value &operator*()  { Evaluate(); return  _value; }
    Value *operator->() { Evaluate(); return &_value; }

private:
    void Evaluate()
    {
        if (!_evaluated)
        {
            _value = _function();
            _evaluated = true;
        }
    }

    std::function<Value()> _function;
    Value _value;
    bool _evaluated;
};

例如用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Noisy
{
public:
    Noisy(int i = 0) : _i(i)
    {
        std::cout <<"Noisy(" << _i <<")"  << std::endl;
    }
    Noisy(const Noisy &that) : _i(that._i)
    {
        std::cout <<"Noisy(const Noisy &)" << std::endl;
    }
    ~Noisy()
    {
        std::cout <<"~Noisy(" << _i <<")" << std::endl;
    }

    void MakeNoise()
    {
        std::cout <<"MakeNoise(" << _i <<")" << std::endl;
    }
private:
    int _i;
};  

int main()
{
    Lazy<Noisy> n = [] () { return Noisy(10); };

    std::cout <<"about to make noise" << std::endl;

    n->MakeNoise();
    (*n).MakeNoise();
    auto &nn = *n;
    nn.MakeNoise();
}

上述代码应在控制台上生成以下消息:

1
2
3
4
5
6
7
8
Noisy(0)
about to make noise
Noisy(10)
~Noisy(10)
MakeNoise(10)
MakeNoise(10)
MakeNoise(10)
~Noisy(10)

注意,在访问变量之前,不会调用打印Noisy(10)的构造函数。

不过,这门课还远远不够完美。首先,必须在成员初始化时调用Value的默认构造函数(在本例中,打印Noisy(0))。我们可以使用指向_value的指针,但我不确定它是否会影响性能。


C++0X很好,所有…但是对于我们这些生活在当下的人来说,你拥有了BoostLambda图书馆和BoostPhoenix。两者都是为了将大量的函数编程引入C++。


一切皆有可能。

这完全取决于你的意思:

1
2
3
4
5
6
7
8
9
class X
{
     public: static X& getObjectA()
     {
          static X instanceA;

          return instanceA;
     }
};

这里我们有一个全局变量的影响,它在第一次使用时被延迟地评估。

按照问题中的新要求。偷了康拉德·鲁道夫的设计并扩展。

懒惰的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template<typename O,typename T1,typename T2>
struct Lazy
{
    Lazy(T1 const& l,T2 const& r)
        :lhs(l),rhs(r) {}

    typedef typename O::Result  Result;
    operator Result() const
    {
        O   op;
        return op(lhs,rhs);
    }
    private:
        T1 const&   lhs;
        T2 const&   rhs;
};

如何使用它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
namespace M
{
    class Matrix
    {
    };
    struct MatrixAdd
    {
        typedef Matrix  Result;
        Result operator()(Matrix const& lhs,Matrix const& rhs) const
        {
            Result  r;
            return r;
        }
    };
    struct MatrixSub
    {
        typedef Matrix  Result;
        Result operator()(Matrix const& lhs,Matrix const& rhs) const
        {
            Result  r;
            return r;
        }
    };
    template<typename T1,typename T2>
    Lazy<MatrixAdd,T1,T2> operator+(T1 const& lhs,T2 const& rhs)
    {
        return Lazy<MatrixAdd,T1,T2>(lhs,rhs);
    }
    template<typename T1,typename T2>
    Lazy<MatrixSub,T1,T2> operator-(T1 const& lhs,T2 const& rhs)
    {
        return Lazy<MatrixSub,T1,T2>(lhs,rhs);
    }
}


在C++ 11中,类似于HaPayy的懒惰评估可以通过使用STD::SyrdJyEngor来实现。您仍然需要将计算封装在lambdas中,但要注意记住:

1
std::shared_future<int> a = std::async(std::launch::deferred, [](){ return 1+1; });

下面是一个完整的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <future>

#define LAZY(EXPR, ...) std::async(std::launch::deferred, [__VA_ARGS__](){ std::cout <<"evaluating"#EXPR << std::endl; return EXPR; })

int main() {
    std::shared_future<int> f1 = LAZY(8);
    std::shared_future<int> f2 = LAZY(2);
    std::shared_future<int> f3 = LAZY(f1.get() * f2.get(), f1, f2);

    std::cout <<"f3 =" << f3.get() << std::endl;
    std::cout <<"f2 =" << f2.get() << std::endl;
    std::cout <<"f1 =" << f1.get() << std::endl;
    return 0;
}


因为它将在C++0x中通过lambda表达式来完成。


使用一个非常简单的lazy evaluation定义,也就是直到需要时才对值进行评估,我想说,可以通过使用指针和宏(用于语法sugar)来实现这一点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include <stdatomic.h>

#define lazy(var_type) lazy_ ## var_type

#define def_lazy_type( var_type ) \
    typedef _Atomic var_type _atomic_ ## var_type; \
    typedef _atomic_ ## var_type * lazy(var_type);  //pointer to atomic type


#define def_lazy_variable(var_type, var_name ) \
    _atomic_ ## var_type _ ## var_name; \
    lazy_ ## var_type var_name = & _ ## var_name;


#define assign_lazy( var_name, val ) atomic_store( & _ ## var_name, val )
#define eval_lazy(var_name) atomic_load( &(*var_name) )

#include <stdio.h>

def_lazy_type(int)

void print_power2 ( lazy(int) i )
{
      printf("%d
"
, eval_lazy(i) * eval_lazy(i) );
}

typedef struct {
    int a;
} simple;

def_lazy_type(simple)

void print_simple ( lazy(simple) s )
{
    simple temp = eval_lazy(s);
    printf("%d
"
, temp.a );
}


#define def_lazy_array1( var_type, nElements, var_name ) \
    _atomic_ ## var_type  _ ## var_name [ nElements ]; \
    lazy(var_type) var_name = _ ## var_name;


int main ( )
{
    //declarations
    def_lazy_variable( int, X )
    def_lazy_variable( simple, Y)
    def_lazy_array1(int,10,Z)
    simple new_simple;

    //first the lazy int
    assign_lazy(X,111);
    print_power2(X);

    //second the lazy struct
    new_simple.a = 555;
    assign_lazy(Y,new_simple);
    print_simple ( Y );

    //third the array of lazy ints
    for(int i=0; i < 10; i++)
    {
        assign_lazy( Z[i], i );
    }

    for(int i=0; i < 10; i++)
    {
        int r = eval_lazy( &Z[i] ); //must pass with &
        printf("%d
"
, r );
    }

    return 0;
}

您会注意到在函数print_power2中有一个名为eval_lazy的宏,它只会在实际需要之前取消对指针的引用,以获取值。惰性类型是原子访问的,所以它是完全线程安全的。


让我们把哈斯克尔作为我们的灵感-它是懒惰的核心。另外,让我们记住C中的linq是如何以一元(urgh-这里是单词-sorry)的方式使用枚举器的。最后,让我们记住,协程应该为程序员提供什么。即计算步骤(如生产者-消费者)之间的脱钩。让我们试着想想协程和懒惰的评估之间的关系。好的。

所有这些似乎都有某种关联。好的。

接下来,让我们尝试提取"懒惰"的个人定义。好的。

一种解释是:在执行计算之前,我们希望以一种可组合的方式陈述我们的计算。我们用来组成完整解的某些部分可能很好地利用巨大(有时是无限的)数据源,我们的充分计算也会产生有限或无限的结果。好的。

让我们具体化一些代码。我们需要一个例子!在这里,我选择FizzBuzz"问题"作为例子,仅仅是因为它有一些好的、懒惰的解决方案。好的。

在哈斯克尔,情况如下:好的。

1
2
3
4
5
6
7
8
9
10
11
12
module FizzBuzz
( fb
)
where
fb n =
    fmap merge fizzBuzzAndNumbers
    where
        fizz = cycle ["","","fizz"]
        buzz = cycle ["","","","","buzz"]
        fizzBuzz = zipWith (++) fizz buzz
        fizzBuzzAndNumbers = zip [1..n] fizzBuzz
        merge (x,s) = if length s == 0 then show x else s

haskell函数cycle创建了一个无限列表(当然是懒惰的!)从有限列表中通过简单地永远重复有限列表中的值。以一种热切的编程风格,写这样的东西会敲响警钟(内存溢出,无限循环!)但在一种懒惰的语言中却不是这样。诀窍是,不立即计算懒惰列表。也许永远不会。通常只有后续代码需要它的时候。好的。

上面的where块中的第三行创建了另一个懒惰!!list,通过将无限列表fizzbuzz组合在一起,使用单个两元素配方"将一个字符串元素从任一输入列表连接到一个字符串中"。同样,如果要立即对其进行评估,我们必须等待计算机耗尽资源。好的。

在第4行中,我们用我们的无限懒惰列表fizzbuzz创建有限懒惰列表[1..n]的成员的元组。结果仍然是懒惰的。好的。

即使在我们的fb功能的主体中,也没有必要变得急切。整个函数返回一个包含解决方案的列表,这个列表本身也是懒惰的。你也可以把fb 50的结果看作是一种计算,稍后你可以(部分)对其进行评估。或者与其他东西结合,导致更大的(懒惰的)评价。好的。

因此,为了开始我们的C++版本的"FiZuBuz",我们需要考虑如何将我们的计算的部分步骤结合到更大的计算位中,每一个都根据需要从先前的步骤中提取数据。好的。

你可以从我的要点中看到整个故事。好的。

下面是代码背后的基本思想:好的。

借鉴C和LINQ,我们"发明"了一个有状态的通用类型Enumerator,它拥有-部分计算的当前值-部分计算的状态(因此我们可以生成后续值)-worker函数,它生成下一个状态、下一个值和一个bool,该bool指示是否有更多的数据或枚举是否结束。好的。

为了利用.的力量来组成Enumerator实例,该类还包含了从haskell类型类(如FunctorApplicative中借用的函数。好的。

枚举器的worker函数的形式始终是:S -> std::tuple,其中S是表示状态的泛型类型变量,T是表示值的泛型类型变量——计算步骤的结果。好的。

所有这些都在Enumerator类定义的第一行中可见。好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
template <class T, class S>
class Enumerator
{
public:
    typedef typename S State_t;
    typedef typename T Value_t;
    typedef std::function<
        std::tuple<bool, State_t, Value_t>
        (const State_t&
            )
    > Worker_t;

    Enumerator(Worker_t worker, State_t s0)
        : m_worker(worker)
        , m_state(s0)
        , m_value{}
    {
    }
    // ...
};

所以,我们只需要创建一个特定的枚举器实例,我们需要创建一个辅助函数,具有初始状态,并用这两个参数创建一个Enumerator的实例。好的。

这里的示例函数range(first,last)创建了有限的值范围。这与haskell世界中的一个懒惰列表相对应。好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
template <class T>
Enumerator<T, T> range(const T& first, const T& last)
{
    auto finiteRange =
        [first, last](const T& state)
    {
        T v = state;
        T s1 = (state < last) ? (state + 1) : state;
        bool active = state != s1;
        return std::make_tuple(active, s1, v);
    };
    return Enumerator<T,T>(finiteRange, first);
}

我们可以利用这个函数,例如:auto r1 = range(size_t{1},10);—我们已经用10个元素创建了一个懒惰的列表!好的。

现在,我们的"哇"体验所缺少的只是看看如何组成枚举器。回到haskells cycle函数,有点酷。在我们的C++世界里会是什么样子?这里是:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
template <class T, class S>
auto
cycle
( Enumerator<T, S> values
) -> Enumerator<T, S>
{
    auto eternally =
        [values](const S& state) -> std::tuple<bool, S, T>
    {
        auto[active, s1, v] = values.step(state);
        if (active)
        {
            return std::make_tuple(active, s1, v);
        }
        else
        {
            return std::make_tuple(true, values.state(), v);
        }
    };
    return Enumerator<T, S>(eternally, values.state());
}

它接受一个枚举器作为输入并返回一个枚举器。局部(lambda)函数eternally只需在输入枚举的值用完时将其重置为起始值,voil_-我们有一个无限的、不断重复的列表版本,我们将其作为参数提供::auto foo = cycle(range(size_t{1},3));并且我们已经可以无耻地编写我们懒散的"计算"。好的。

zip是一个很好的例子,它表明我们还可以从两个输入枚举器创建一个新的枚举器。结果枚举器生成的值与两个输入枚举器中较小的一个(具有2个元素的元组,每个输入枚举器一个)的值一样多。我已经在class Enumerator内部实现了zip。它看起来是这样的:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// member function of class Enumerator<S,T>
template <class T1, class S1>
auto
zip
( Enumerator<T1, S1> other
) -> Enumerator<std::tuple<T, T1>, std::tuple<S, S1> >
{
    auto worker0 = this->m_worker;
    auto worker1 = other.worker();
    auto combine =
        [worker0,worker1](std::tuple<S, S1> state) ->
        std::tuple<bool, std::tuple<S, S1>, std::tuple<T, T1> >
    {
        auto[s0, s1] = state;
        auto[active0, newS0, v0] = worker0(s0);
        auto[active1, newS1, v1] = worker1(s1);
        return std::make_tuple
            ( active0 && active1
            , std::make_tuple(newS0, newS1)
            , std::make_tuple(v0, v1)
            );
    };
    return Enumerator<std::tuple<T, T1>, std::tuple<S, S1> >
        ( combine
        , std::make_tuple(m_state, other.state())
        );
}

请注意,"合并"是如何同时合并两个源的状态和两个源的值的。好的。

因为这篇文章已经是tl;dr;对很多人来说,这里是…好的。

总结好的。

是的,懒惰评估可以在C++中实现。在这里,我借用了Haskell的函数名和C枚举器和Linq的范例。顺便说一句,可能和土鳖有相似之处。我认为他们采用了类似的方法。好的。

我的实现(见上面的gist链接)只是一个原型——而不是生产代码,顺便说一句,所以我没有任何保证。不过,它还是一个很好的演示代码,可以让一般的想法通过。好的。

如果没有FiZuz的C++版本,这个答案会是什么呢?这里是:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
std::string fizzbuzz(size_t n)
{
    typedef std::vector<std::string> SVec;
    // merge (x,s) = if length s == 0 then show x else s
    auto merge =
        [](const std::tuple<size_t, std::string> & value)
        -> std::string
    {
        auto[x, s] = value;
        if (s.length() > 0) return s;
        else return std::to_string(x);
    };

    SVec fizzes{"","","fizz" };
    SVec buzzes{"","","","","buzz" };

    return
    range(size_t{ 1 }, n)
    .zip
        ( cycle(iterRange(fizzes.cbegin(), fizzes.cend()))
          .zipWith
            ( std::function(concatStrings)
            , cycle(iterRange(buzzes.cbegin(), buzzes.cend()))
            )
        )
    .map<std::string>(merge)
    .statefulFold<std::ostringstream&>
    (
        [](std::ostringstream& oss, const std::string& s)
        {
            if (0 == oss.tellp())
            {
                oss << s;
            }
            else
            {
                oss <<"," << s;
            }
        }
        , std::ostringstream()
    )
    .str();
}

还有…更进一步地将这一点带回家-这里是FizzBuzz的变体,它向调用者返回一个"无限列表":好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
typedef std::vector<std::string> SVec;
static const SVec fizzes{"","","fizz" };
static const SVec buzzes{"","","","","buzz" };

auto fizzbuzzInfinite() -> decltype(auto)
{
    // merge (x,s) = if length s == 0 then show x else s
    auto merge =
        [](const std::tuple<size_t, std::string> & value)
        -> std::string
    {
        auto[x, s] = value;
        if (s.length() > 0) return s;
        else return std::to_string(x);
    };

    auto result =
        range(size_t{ 1 })
        .zip
        (cycle(iterRange(fizzes.cbegin(), fizzes.cend()))
            .zipWith
            (std::function(concatStrings)
                , cycle(iterRange(buzzes.cbegin(), buzzes.cend()))
            )
        )
        .map<std::string>(merge)
        ;
    return result;
}

这是值得展示的,因为您可以从中学习如何回避这个问题:函数的确切返回类型是什么(因为它仅取决于函数的实现,即代码如何组合枚举器)。好的。

它还表明,我们必须将向量fizzesbuzzes移动到函数范围之外,以便它们仍然存在,当最终在外部时,懒惰的机制产生值。如果我们不这样做,iterRange(..)代码将把迭代器存储到早已消失的向量中。好的。好啊。