A way of achieving lazy evaluation in C++
所以我回答了一个关于懒惰评估的问题(在这里,我的答案是过分的,但这个想法似乎很有趣),它使我思考如何懒惰的评价可能在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 44 45 | #include <iostream> #include <functional> #include <memory> #include <string> #define LAZY(E) lazy<decltype((E))>{[&](){ return E; }} template<class T> class lazy { private: typedef std::function<std::shared_ptr<T>()> thunk_type; mutable std::shared_ptr<thunk_type> thunk_ptr; public: lazy(const std::function<T()>& x) : thunk_ptr( std::make_shared<thunk_type>([x](){ return std::make_shared<T>(x()); })) {} const T& operator()() const { std::shared_ptr<T> val = (*thunk_ptr)(); *thunk_ptr = [val](){ return val; }; return *val; } T& operator()() { std::shared_ptr<T> val = (*thunk_ptr)(); *thunk_ptr = [val](){ return val; }; return *val; } }; void log(const lazy<std::string>& msg) { std::cout << msg() << std::endl; } int main() { std::string hello ="hello"; std::string world ="world"; auto x = LAZY((std::cout <<"I was evaluated! ", hello +"," + world +"!")); log(x); log(x); log(x); log(x); return 0; } |
我在设计中关心的一些事情。
- decltype有一些奇怪的规则。我对decltype的用法有任何疑问吗?我在lazy宏中的e周围添加了额外的括号,以确保单个名称得到公平的处理,就像vec[10]一样。还有其他事情我不负责吗?
- 在我的例子中有很多间接的层次。这似乎是可以避免的。
- 这是否正确地记忆了结果,以便无论什么或有多少东西引用了lazy值,它只评估一次(这一次我很有信心,但lazy评估加上大量共享指针可能会给我一个循环)
你有什么想法?
是的,你有的是懒惰。基本上,只需传递一个计算参数而不是参数的函数。在计算之后,对象将被计算出的值替换。基本上就是这样,是的,像这样实现,使用引用计数指针,这是非常昂贵的。
记忆化是一个古老的术语,它通常意味着建立一个函数的结果。没有现代语言能做到这一点(可能是序言),这是非常昂贵的。在lambda提升过程中实现了完全的懒散(从不计算两次一件事),即消除自由变量(将其作为参数)。在完全懒惰的lambda提升中,它是提升的最大自由表达式(例如x是自由的,因此sqrt x的出现被新参数sqrt x替换)。还有所谓的最优约简。
我不认为还有其他的方法。为什么在像haskell这样的懒惰的函数语言中速度更快?好吧,基本上,没有引用计数指针,然后有严格性分析(严格与懒惰相反),这允许编译器预先知道一些东西是严格评估的,取消绑定严格评估的值,这些值是已知的机器类型…更不用说其他典型的函数式编程语言优化…但从本质上讲,如果你看一个图缩减机的实现,如果你看栈是如何发展的,你会发现基本上你是在栈上传递函数而不是参数,基本上就是这样。
现在,在这些机器中,计算参数的节点将被其值覆盖。所以您可能缺少一个优化,但在类型安全的上下文中是不可能的。
假设所有的"节点",其中一个主超类的子类,称为"节点",它只有一个虚拟函数来计算值…然后它可能被另一个"覆盖",它将返回已经计算的值。有了函数指针,这就是为什么他们说haskell的stg机器是"无标签的"(无脊椎无标签的g机器),因为他们不标记数据元素,而是使用一个函数指针来计算或返回值。
我认为它不能像在Haskell中那样在C++中做得差不多。除非我们开始考虑用完全不同的方式来实现C++(可以而且应该做)。我们习惯了这样复杂的序言、词尾、复杂的调用约定等。C/C++中的函数调用过于官僚化。
现在,当你感到懒惰时阅读的书绝对是西蒙·佩顿·琼斯的《函数式编程语言的实现》。然而,在免费提供的文章"在库存硬件上实现函数语言:无脊椎的无标签G-machine"中描述了现代实现,这对于了解实现优化是非常好的,但是另一个是为了了解基本原理而阅读的。
- 您可能希望将
thunk_type 和引用作为单独的对象。现在,lazy 的拷贝将从原产地评估中一无所获。但在这种情况下,您将获得额外的间接访问权。 - 有时,您可以通过简单地使用模板来避免包装到std::函数中。
- 我不确定值是否需要共享。也许打电话的人应该决定。
- 您将在每个访问上生成新的闭包。
考虑下一个修改:
1 2 3 4 5 6 7 8 | template<typename F> lazy(const F& x) : thunk_ptr([&x,&this](){ T val = (*x)(); thunk_ptr = [val]() { return val; }; return val; }) {} |
或者其他实现可能看起来像:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | template<typename F> auto memo(const F &x) -> std::function<const decltype(x()) &()> { typedef decltype(x()) return_type; typedef std::function<const return_type &()> thunk_type; auto thunk_ptr = std::make_shared<thunk_type>(); auto *thunk_cptr = thunk_ptr.get(); // note that this lambda is called only from scope which holds thunk_ptr *thunk_ptr = [thunk_cptr, &x]() { auto val = std::move(x()); auto &thunk = *thunk_cptr; thunk = [val]() { return val; }; // at this moment we can't refer to catched vars return thunk(); }; return [thunk_ptr]() { return (*thunk_ptr)(); }; }; |
这是我需要的懒惰的另一个方面。
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 | // REMARK: Always use const for lazy objects. Any, even const operation coming from ValueType called over Lazy<ValueType> freezes it. template < typename ValueType > struct Lazy { typedef ValueType Value; typedef std::function<Value()> Getter; Lazy( const Value& value = Value() ) : m_value( value ) { } Lazy( Value&& value ) : m_value( value ) { } Lazy( Lazy& other ) : Lazy( const_cast<const Lazy&>(other) ) { } Lazy( const Lazy& other ) = default; Lazy( Lazy&& other ) = default; Lazy& operator = ( const Lazy& other ) = default; Lazy& operator = ( Lazy&& other ) = default; template < typename GetterType, typename = typename std::enable_if<std::is_convertible<GetterType,Getter>::value>::type > Lazy( GetterType&& getter ) : m_pGetter( std::make_shared<Getter>( std::move(getter) ) ) { } void Freeze() { if ( m_pGetter ) { m_value = (*m_pGetter)(); m_pGetter.reset(); } } operator Value () const { return m_pGetter ? (*m_pGetter)() : m_value; } operator Value& () { Freeze(); return m_value; } private: Value m_value; std::shared_ptr<Getter> m_pGetter; }; |
使用方法如下:
1 2 3 4 5 6 7 8 9 10 11 | template < typename VectorType, typename VectorIthValueGetter = std::function<typename VectorType::const_reference (const size_t)> > static auto MakeLazyConstRange( const VectorType& vector ) -> decltype( boost::counting_range( Lazy<size_t>(), Lazy<size_t>() ) | boost::adaptors::transformed( VectorIthValueGetter() ) ) { const Lazy<size_t> bb( 0 ) ; const Lazy<size_t> ee( [&] () -> size_t { return vector.size(); } ); const VectorIthValueGetter tt( [&] (const size_t i) -> typename VectorType::const_reference { return vector[i]; } ); return boost::counting_range( bb, ee ) | boost::adaptors::transformed( tt ); } |
后来:
1 2 3 4 5 6 7 8 9 10 11 | std::vector<std::string> vv; boost::any_range<const std::string&, boost::forward_traversal_tag, const std::string&, int> rr = MakeLazyConstRange( vv ); vv.push_back("AA" ); vv.push_back("BB" ); vv.push_back("CC" ); vv.push_back("DD" ); for ( const auto& next : rr ) std::cerr <<"----" << next << std::endl; |
在Lazy类的实现中,我采用了另一种方式——lambda函数不返回值,它将其作为参数。它有助于实现一些好处:
另外,这个版本应该是线程安全的(如果我做错了,请纠正我)。一个仍然保留的需求-默认构造函数。
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 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | #pragma once #include <mutex> #include #include <functional> template <typename T> struct Lazy { using value_type = T; Lazy() : mInitializer(nullptr) {} Lazy(const std::function<void(T&)>& initializer) : mInitializer(std::move(initializer)) , mInitFlag(false) { } Lazy(const Lazy& other) : mInitializer(other.mInitializer) , mInitFlag(other.mInitFlag.load()) , mValue(other.mValue) { } Lazy(Lazy&& other) : mInitializer(std::move(other.mInitializer)) , mInitFlag(other.mInitFlag.load()) , mValue(std::move(other.mValue)) { } Lazy& operator=(const std::function<void(T&)>& initializer) { mInitFlag.store(false); mInitializer = initializer; return *this; }; Lazy& operator=(const Lazy& rhs) { if (this != &rhs) { std::lock_guard<std::mutex> lock(mMutex); mInitializer = rhs.mInitializer; mInitFlag = rhs.mInitFlag.load(); if (mInitFlag) { mValue = rhs.mValue; } } return *this; }; Lazy& operator=(Lazy&& rhs) { if (this != &rhs) { std::lock_guard<std::mutex> lock(mMutex); mInitializer = std::move(rhs.mInitializer); mInitFlag = rhs.mInitFlag.load(); if (mInitFlag) { mValue = std::move(rhs.mValue); } } return *this; }; inline operator T&() { return get(); } inline operator const T&() const { return get(); } inline T& get() { return const_cast<T&>(_getImpl()); } inline const T& get() const { return _getImpl(); } private: const T& _getImpl() const { if (mInitializer != nullptr && mInitFlag.load() == false) { std::lock_guard<std::mutex> lock(mMutex); if (mInitFlag.load() == false) { mInitializer(mValue); mInitFlag.store(true); } } return mValue; } mutable std::mutex mMutex; std::function<void(T&)> mInitializer; mutable std::atomic_bool mInitFlag; mutable T mValue; // Value should be after mInitFlag due initialization order }; |
使用样品:
1 2 3 4 5 6 | using ValuesList = std::vector<int>; Lazy<ValuesList> lazyTest = [](ValuesList& val) { val.assign({1, 2, 3, 4, 5}); }; const Lazy<ValuesList> lazyTestConst = lazyTest; ValuesList& value = lazyTest; const ValuesList& cvalue = lazyTestConst; |
Boost凤凰库实现了LaZyess,在其他FP的细节中,但我没有使用过我自己,我不确定它与C++ 11有多好,或者它至少部分地使它达到了2011标准。
http://www.boost.org/doc/libs/1_43_0/libs/spirit/phoenix/doc/html/index.html