C ++不像Haskell那样对延迟评估提供本机支持。
我想知道是否有可能以合理的方式在C ++中实现惰性评估。如果是,您将如何做?
编辑:我喜欢康拉德·鲁道夫的答案。
我想知道是否有可能以更通用的方式实现它,例如,通过使用参数化的类lazy,该类实际上对T有效,而matrix_add对矩阵有效。
对T的任何操作都将返回惰性。唯一的问题是将参数和操作代码存储在惰性内部。谁能看到如何改善这一点?
Answers:
我想知道是否有可能以合理的方式在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
到plain的隐式转换运算符matrix
。这样,您可以链接多个操作(当然,通过提供适当的重载)。仅在将最终结果分配给matrix
实例时进行评估。
编辑我应该更加明确。照原样,该代码没有意义,因为尽管评估是惰性进行的,但它仍在同一表达式中进行。特别是,除非matrix_add
更改结构以允许链式添加,否则另一个添加将评估此代码。C ++ 0x通过允许使用可变参数模板(即可变长度的模板列表)极大地促进了这一点。
但是,在以下一种非常简单的情况下,此代码实际上将具有真正的直接好处:
int value = (A + B)(2, 3);
在此,假设A
和B
是二维矩阵,并且使用Fortran表示法进行了解引用,即,上述方法从矩阵和中计算出一个元素。添加整个矩阵当然是浪费的。matrix_add
进行救援:
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);
}
};
其他例子比比皆是。我刚刚记得我不久前实现了一些相关的东西。基本上,我必须实现一个字符串类,该字符串类应遵循固定的预定义接口。但是,我特定的字符串类处理的是实际上并未存储在内存中的巨大字符串。通常,用户只需要使用function从原始字符串访问较小的子字符串infix
。我为我的字符串类型重载了此函数,以返回一个代理,该代理保存对我的字符串的引用以及所需的开始和结束位置。仅当实际使用此子字符串时,它才查询C API以检索字符串的此部分。
1 : 1 : [ a+b | (a,b) <- zip fib (tail fib) ]
–使用此定义执行将take 50 fib
清楚地表明不会进行重新评估,否则运行时间将是指数级的,而不是线性的(观察到的)。
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; }
};
这将存储任何加法运算,甚至是嵌套的加法运算,从以下对简单点类型的operator +的定义可以看出:
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
代替,但是我不确定它是否会影响性能。
Johannes的答案是有效的。但是,当涉及到更多的括号时,它并没有达到预期的效果。这是一个例子。
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);
}
现在,编译器可以正确处理上述情况,并且不需要隐式转换!
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 ++ 11中,可以使用std :: shared_future实现类似于hiapay答案的惰性评估。您仍然必须将计算封装在lambda中,但要注意以下事项:
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如何以单子形式(枚举-这是单词-对不起)使用枚举器。最后,让我们牢记协程应该为程序员提供什么。即,计算步骤(例如生产者消费者)之间的解耦。并让我们考虑一下协程与懒惰评估的关系。
以上所有似乎都与之相关。
接下来,让我们尝试提取对“懒惰”的个人定义。
一种解释是:我们想在计算之前以一种可组合的方式陈述我们的计算执行。我们用来构成完整解决方案的那些部分中的某些部分很可能会利用庞大的(有时是无限的)数据源,而我们的完整计算也会产生有限或无限的结果。
让我们具体化一些代码。我们需要一个例子!在这里,我以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
上面块中的第三行创建了另一个惰性!列表,通过组合无限列表fizz
并buzz
通过单个两个元素,配方“将来自任一输入列表的字符串元素连接为单个字符串”。同样,如果要立即对此进行评估,我们将不得不等待计算机资源耗尽。
在第四行中,我们[1..n]
使用无限惰性列表创建有限惰性列表的成员的元组fizzbuzz
。结果仍然是懒惰的。
即使在我们fb
职能的主体中,也没有必要变得渴望。整个函数返回带有解决方案的列表,该列表本身又是-lazy-的。您也可以将的结果认为是fb 50
可以稍后(部分)评估的计算结果。或与其他内容结合使用,可以得出更大(懒惰)的评估结果。
因此,为了开始使用我们的C ++版本的“ fizzbuzz”,我们需要考虑如何将部分计算步骤组合为更大的计算量的方法,每个方法都根据需要从先前的步骤中提取数据。
您可以在我的要旨中查看整个故事。
下面是代码背后的基本思想:
从C#和Linq借用,我们“发明”了一个有状态的通用类型Enumerator
,该类型具有:
-部分计算的当前值
-部分计算的状态(因此我们可以产生后续值)
-辅助函数,其产生下一个状态,下一个值和一个布尔值,该布尔值说明是否有更多数据或枚举是否结束。
为了能够Enumerator<T,S>
借助.
(dot)的功能来组成实例,该类还包含从Haskell类型类(例如Functor
和)借来的函数Applicative
。
枚举器的worker函数始终采用以下形式:S -> std::tuple<bool,S,T
其中S
,泛型类型变量表示状态,而T
是表示值的通用类型变量-计算步骤的结果。
所有这些在Enumerator
类定义的第一行中已经可见。
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世界中的惰性列表。
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个元素的惰性列表!
现在,我们“哇”的体验所缺少的只是看我们如何构成枚举器。回到Haskellscycle
函数,这很酷。在我们的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)函数eternally
只要将输入枚举数用尽,就会简单地将输入枚举值重置为初始值,并且会产生错误-我们提供了一个无穷无尽的重复列表,作为参数::auto foo = cycle(range(size_t{1},3));
而且,我们已经可以毫不客气地编写懒惰的“计算”。
zip
这是一个很好的例子,说明我们还可以从两个输入枚举器创建一个新的枚举器。所得的枚举器产生的值与任何一个输入枚举器中的较小值(具有2个元素的元组,每个输入枚举器一个)有关。我已经zip
在内部实现了class Enumerator
。看起来是这样的:
// 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;
}
值得展示,因为您可以从中学习如何回避该函数的确切返回类型是什么(因为它仅取决于函数的实现,即代码如何结合枚举器)。
它还表明,我们必须将向量fizzes
和buzzes
函数范围移到外部,因此当最终在外部时,惰性机制会产生值,因此它们仍然存在。如果我们没有这样做,那么iterRange(..)
代码将把迭代器存储到向量中,而这些向量早已不复存在。
使用一个非常简单的惰性求值定义,即直到需要时才求值,我可以说可以通过使用指针和宏(用于语法糖)来实现这一点。
#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
,它仅在实际需要时才取消引用指针以获取值。惰性类型是原子访问的,因此它是完全线程安全的。