题
C ++没有对延迟评估的原生支持(正如Haskell所做的那样)。
我想知道是否可以以合理的方式在C ++中实现延迟评估。如果是的话,你会怎么做?
编辑:我喜欢Konrad Rudolph的回答。我想知道是否可以以更通用的方式实现它,例如通过使用一个基本上适用于矩阵的参数化类,就像matrix_add对矩阵一样工作。
对T的任何操作都会返回lazy。唯一的问题是将参数和操作代码存储在惰性本身中。有谁能看到如何改善这个?
解决方案
我想知道是否可以以合理的方式在C ++中实现延迟评估。如果是的话,你会怎么做?
是的,这是可能的并且经常完成,例如用于矩阵计算。促进这一点的主要机制是运算符重载。考虑矩阵加法的情况。函数的签名通常如下所示:
matrix operator +(matrix const& a, matrix const& b);
现在,为了使这个函数变得懒惰,它足以返回代理而不是实际结果:
struct matrix_add;
matrix_add operator +(matrix const& a, matrix const& b) {
return matrix_add(a, b);
}
现在需要做的就是编写这个代理:
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
的隐式转换运算符。这样,您可以链接多个操作(当然,通过提供适当的重载)。只有在将最终结果分配给A
实例时才会进行评估。
编辑我本来应该更明确。实际上,代码没有任何意义,因为尽管评估是懒散的,但它仍然发生在同一个表达式中。特别是,除非将B
结构更改为允许链接添加,否则另一个添加将评估此代码。 C ++ 0x通过允许可变参数模板(即可变长度的模板列表)极大地促进了这一点。
然而,这个代码实际上具有真正直接好处的一个非常简单的例子如下:
int value = (A + B)(2, 3);
这里,假设infix
和<=>是二维矩阵,并且以Fortran表示法进行解除引用,即上面计算矩阵和中的一个元素。添加整个矩阵当然是浪费。 <=>救援:
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);
}
};
其他例子比比皆是。我记得不久前我已经实现了一些相关的东西。基本上,我必须实现一个应该遵循固定的预定义接口的字符串类。但是,我的特定字符串类处理的是实际上并未存储在内存中的大字符串。通常,用户只需使用函数<=>访问原始字符串中的小子串。我为我的字符串类型重载了这个函数,以返回一个代理,该代理保存对我的字符串的引用,以及所需的开始和结束位置。只有在实际使用此子字符串时,它才会查询C API以检索字符串的这一部分。
其他提示
Boost.Lambda非常好,但 Boost.Proto 正好你在寻找什么。它已经有所有 C ++运算符的重载,默认情况下,在调用proto::eval()
时执行它们的常用功能,但可以更改。
Konrad已经解释过的内容可以进一步支持运算符的嵌套调用,所有这些都是懒惰执行的。在Konrad的例子中,他有一个表达式对象,它可以存储两个参数,正好是一个操作的两个操作数。问题是它只会懒惰地执行一个子表达式,这很好地解释了懒惰评估中的概念,简单来说,但并没有大幅提高性能。另一个示例也很好地展示了如何应用operator()
仅使用该表达式对象添加一些元素。但是为了评估任意复杂的表达式,我们需要一些能够存储结构的机制。我们无法绕过模板来做到这一点。它的名字是expression templates
。这个想法是一个模板化的表达式对象可以递归地存储某个任意子表达式的结构,就像一棵树,其中操作是节点,操作数是子节点。对于我今天刚刚发现的非常的好解释(在我编写下面的代码后几天),请参阅这里。
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; }
};
这将存储任何添加操作,甚至是嵌套的操作,如下面的操作符+对于简单点类型的定义所示:
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);
}
现在,如果你有
Point p1 = { 1, 2 }, p2 = { 3, 4 }, p3 = { 5, 6 };
p1 + (p2 + p3); // returns AddOp< Point, AddOp<Point, Point> >
您现在只需要重载operator =并为Point类型添加合适的构造函数并接受AddOp。将其定义更改为:
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中:
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 在真实世界的应用程序中,正确完成延迟评估的示例。这真是令人敬畏。
我正在考虑实现一个使用std::function
的模板类。课程应该或多或少看起来像这样:
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;
};
例如用法:
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();
}
上面的代码应该在控制台上产生以下消息:
Noisy(0)
about to make noise
Noisy(10)
~Noisy(10)
MakeNoise(10)
MakeNoise(10)
MakeNoise(10)
~Noisy(10)
请注意,在访问变量之前,不会调用构造函数Noisy(10)
。
但这个课程远非完美。第一件事是Value
的默认构造函数必须在成员初始化时调用(在这种情况下打印Noisy(0)
)。我们可以使用指针代替_value
,但我不确定它是否会影响性能。
约翰内斯的回答是有效的。但是当谈到更多的括号时,它并不像希望那样有效。这是一个例子。
Point p1 = { 1, 2 }, p2 = { 3, 4 }, p3 = { 5, 6 }, p4 = { 7, 8 };
(p1 + p2) + (p3+p4)// it works ,but not lazy enough
因为三个重载+运算符没有覆盖案例
AddOp<Llhs,Lrhs>+AddOp<Rlhs,Rrhs>
所以编译器必须将(p1 + p2)或(p3 + p4)转换为Point,这不够懒惰。当编译器决定转换哪个时,它会抱怨。因为没有一个比另一个好。 这是我的扩展:添加另一个重载的运算符+
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!
C ++ 0x很好,所有....但对于我们这些生活在现在的人来说,你有Boost lambda库和Boost Phoenix。两者都旨在为C ++带来大量的函数式编程。
一切皆有可能。
这完全取决于你的意思:
class X
{
public: static X& getObjectA()
{
static X instanceA;
return instanceA;
}
};
这里我们有一个全局变量的影响,该变量在首次使用时被懒惰地评估。
正如问题中新要求的那样 并且窃取了Konrad Rudolph的设计并扩展了它。
懒惰对象:
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;
};
如何使用它:
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 ++ 0x 中完成,通过lambda表达式。
在C ++ 11中,使用std :: shared_future可以实现类似于hiapay答案的懒惰评估。你仍然需要在lambdas中封装计算,但是记忆被处理:
std::shared_future<int> a = std::async(std::launch::deferred, [](){ return 1+1; });
以下是一个完整的例子:
#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;
}
让我们把Haskell作为我们的灵感 - 它是懒惰的核心。 另外,让我们记住C#中的Linq如何在monadic(urgh - 这就是单词 - 抱歉)方式中使用枚举器。 最重要的是,让我们记住,协同程序应该为程序员提供什么协程。即,计算步骤(例如,生产者消费者)彼此分离。 让我们试着想一下协同程序如何与懒惰评估相关联。
以上所有内容似乎都有某种关系。
接下来,让我们尝试提取我们对<!>“lazy <!>”的个人定义。归结为。
一种解释是:我们希望以可组合的方式陈述我们的计算,在执行之前。我们用来构成完整解决方案的部分部分可能很好地利用了巨大的(有时是无限的)数据源,我们的完整计算也会产生有限或无限的结果。
让我们具体化并进入一些代码。我们需要一个例子!在这里,我选择了fizzbuzz <!>“问题<!>”;举个例子,只是因为它有一些很好的,懒惰的解决方案。
在Haskell中,它看起来像这样:
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,通过组合无限列表fizz
和buzz
,通过单个两个元素recipe <!>“;将来自任一输入列表的字符串元素连接成单个字符串<!>”;同样,如果要立即评估,我们将不得不等待我们的计算机耗尽资源。
在第4行中,我们使用无限懒惰列表[1..n]
创建有限惰性列表fizzbuzz
成员的元组。结果仍然是懒惰的。
即使在我们fb
函数的主体中,也没有必要急于求成。整个函数返回一个包含解决方案的列表,该解决方案本身就是-again-lazy。您也可以将fb 50
的结果视为可以(部分)稍后评估的计算结果。或者与其他东西结合,导致更大(懒惰)的评估。
因此,为了开始使用我们的C ++版本的<!>“fizzbuzz <!>”,我们需要考虑如何将计算的部分步骤组合成更大的计算位,每个绘制数据根据需要从以前的步骤。
您可以在我的要点中看到完整的故事。
这里是代码背后的基本思想:
借用C#和Linq,我们<!>发明<!>有状态的通用类型Enumerator
,它包含
- 部分计算的当前值
- 部分计算的状态(因此我们可以生成后续值)
- worker函数,它产生下一个状态,下一个值和一个bool,表示是否有更多数据或枚举是否已经结束。
为了能够通过Enumerator<T,S>
(点)的强大功能组合.
实例,该类还包含从Haskell类型类(例如Functor
和Applicative
)中借用的函数。 / p>
枚举器的worker函数始终采用以下形式:S -> std::tuple<bool,S,T
其中S
是表示状态的泛型类型变量,T
是表示值的泛型类型变量 - 计算步骤的结果。
所有这些在range(first,last)
类定义的第一行中已经可见。
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{}
{
}
// ...
};
因此,我们需要创建一个特定的枚举器实例,我们需要创建一个worker function,具有初始状态并使用这两个参数创建auto r1 = range(size_t{1},10);
的实例。
这里有一个例子 - 函数eternally
创建一个有限范围的值。这对应于Haskell世界中的惰性列表。
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 foo = cycle(range(size_t{1},3));
- 我们创建了一个包含10个元素的惰性列表!
现在,我们的<!>引用了所有内容!哇<!>经验,是看我们如何组成调查员。
回到Haskells zip
函数,这很酷。它在我们的C ++世界中会如何?这是:
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)函数class Enumerator
只需将输入枚举重置为其初始值,只要它用完了值并且voil <!>#224; - 我们有一个无限的,不断重复的列表我们作为参数提供的版本:: fizzes
我们已经可以无耻地组成我们的懒惰的<!>“计算<!>”。
buzzes
是一个很好的例子,表明我们也可以从两个输入枚举器创建一个新的枚举器。生成的枚举器产生的值与任一输入枚举器中的较小者一样多(具有2个元素的元组,每个输入枚举器一个)。我在iterRange(..)
内部实现了<=>。这是它的样子:
// 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的范例来实现它。可能与pythons itertools相似,顺便说一句。我认为他们采用了类似的方法。
我的实现(参见上面的gist链接)只是一个原型 - 而不是生产代码,顺便说一句。因此,我方无任何保证。尽管如此,它还是可以作为演示代码来获得一般性的想法。
如果没有fizzbuz的最终C ++版本,这个答案会是什么呢?这是:
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的一个变种,它返回一个<!>“无限列表<!>”;致来电者:
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;
}
值得一提,因为您可以从中学习如何避免该函数的确切返回类型是什么(因为它取决于函数的实现,即代码如何组合枚举器)。 / p>
此外,它还表明我们必须将向量<=>和<=>移到函数范围之外,这样当它们最终在外部时它们仍然存在,惰性机制产生值。如果我们没有这样做,那么<=>代码会将迭代器存储到很久以前的向量中。
使用惰性求值的一个非常简单的定义,即在需要之前不会对值进行求值,我会说可以通过使用指针和宏来实现这一点(对于语法糖)。
#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\n", 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\n", 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\n", r );
}
return 0;
}
你会注意到函数print_power2
中有一个名为eval_lazy
的宏,它除了取消引用一个指针以在实际需要之前获取值之外什么也没做。惰性类型是以原子方式访问的,因此它完全是线程安全的。