关于c ++ 11:一种在C ++中实现延迟评估的方法

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函数不返回值,它将其作为参数。它有助于实现一些好处:

  • 为封装类型调用move构造函数节省了时间(当initialize函数返回结果时)。
  • 不需要封装类型的复制构造函数和赋值运算符(仅当您希望对Lazy类型执行此操作时)。
  • 另外,这个版本应该是线程安全的(如果我做错了,请纠正我)。一个仍然保留的需求-默认构造函数。

    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