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; }; |
神奇之处在于方法
编辑我应该更明确些。事实上,代码没有任何意义,因为尽管计算发生得很慢,但它仍然发生在同一个表达式中。特别是,另一个加法将评估此代码,除非将
但是,在一个非常简单的情况下,此代码实际上会有一个真正的、直接的好处,如下所示:
1 | int value = (A + B)(2, 3); |
这里,假设
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); } }; |
其他例子不胜枚举。我只记得不久前我已经实现了一些相关的东西。基本上,我必须实现一个字符串类,它应该遵循一个固定的、预定义的接口。然而,我的特殊字符串类处理的是实际上没有存储在内存中的巨大字符串。通常,用户只需使用函数
助推。lambda很不错,但是助推。Proto正是你想要的。它已经具有所有C++运算符的重载,默认情况下,当调用EDCOX1×2调用时,它们可以执行它们通常的函数,但是可以被改变。
Konrad已经解释的内容可以进一步支持嵌套调用操作符,所有这些都是延迟执行的。在Konrad的例子中,他有一个表达式对象,可以为一个操作的两个操作数存储两个参数。问题是,它只会延迟地执行一个子表达式,这很好地解释了延迟评估中的概念,用简单的术语来说,但并不能显著提高性能。另一个例子也很好地展示了如何应用
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!
我正在考虑实现一个使用
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) |
注意,在访问变量之前,不会调用打印
不过,这门课还远远不够完美。首先,必须在成员初始化时调用
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; } |
您会注意到在函数
让我们把哈斯克尔作为我们的灵感-它是懒惰的核心。另外,让我们记住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函数
上面的
在第4行中,我们用我们的无限懒惰列表
即使在我们的
因此,为了开始我们的C++版本的"FiZuBuz",我们需要考虑如何将我们的计算的部分步骤结合到更大的计算位中,每一个都根据需要从先前的步骤中提取数据。好的。
你可以从我的要点中看到整个故事。好的。
下面是代码背后的基本思想:好的。
借鉴C和LINQ,我们"发明"了一个有状态的通用类型
为了利用
枚举器的worker函数的形式始终是: 所有这些都在 所以,我们只需要创建一个特定的枚举器实例,我们需要创建一个辅助函数,具有初始状态,并用这两个参数创建一个 这里的示例函数 我们可以利用这个函数,例如: 现在,我们的"哇"体验所缺少的只是看看如何组成枚举器。回到haskells 它接受一个枚举器作为输入并返回一个枚举器。局部(lambda)函数 请注意,"合并"是如何同时合并两个源的状态和两个源的值的。好的。 因为这篇文章已经是tl;dr;对很多人来说,这里是…好的。 总结好的。 是的,懒惰评估可以在C++中实现。在这里,我借用了Haskell的函数名和C枚举器和Linq的范例。顺便说一句,可能和土鳖有相似之处。我认为他们采用了类似的方法。好的。 我的实现(见上面的gist链接)只是一个原型——而不是生产代码,顺便说一句,所以我没有任何保证。不过,它还是一个很好的演示代码,可以让一般的想法通过。好的。 如果没有FiZuz的C++版本,这个答案会是什么呢?这里是:好的。 还有…更进一步地将这一点带回家-这里是FizzBuzz的变体,它向调用者返回一个"无限列表":好的。 这是值得展示的,因为您可以从中学习如何回避这个问题:函数的确切返回类型是什么(因为它仅取决于函数的实现,即代码如何组合枚举器)。好的。 它还表明,我们必须将向量
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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{}
{
}
// ...
};
2
3
4
5
6
7
8
9
10
11
12
13
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);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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());
}
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
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())
);
}
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
{
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();
}
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
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;
}