Подтвердить что ты не робот

Способ достижения ленивой оценки в С++

Итак, я отвечал на вопрос об ленивой оценке (здесь, мой ответ - это излишество для этого случая, но идея кажется интересной), и это заставило меня задуматься о том, как ленивый оценка может быть выполнена на С++. Я придумал путь, но я не был уверен в всех подводных камнях в этом. Существуют ли другие способы достижения ленивой оценки? как это можно сделать? Каковы подводные камни и этот и другие проекты?

Вот моя идея:

#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!\n", hello + ", " + world + "!"));
    log(x);
    log(x);
    log(x);
    log(x);
    return 0;
}

Некоторые вещи, о которых я беспокоился в своем дизайне.

  • У decltype есть некоторые странные правила. Имеет ли мое использование decltype какие-либо ошибки? Я добавил дополнительные круглые скобки вокруг E в макросе LAZY, чтобы убедиться, что отдельные имена обработаны справедливо, поскольку ссылки, подобные VEC [10], будут. Есть ли другие вещи, которые я не учитываю?
  • В моем примере есть много слоев косвенности. Похоже, этого можно избежать.
  • Правильно ли это запоминание результата, так что независимо от того, сколько или сколько вещей ссылается на ленивое значение, оно будет оцениваться только один раз (это я уверен в ленивой оценке плюс тонны общих указателей, возможно, петля)

Каковы ваши мысли?

4b9b3361

Ответ 1

  • Возможно, вы захотите иметь thunk_type и ссылаться на него как на отдельные объекты. Прямо сейчас копия lazy<T> ничего не получит от оценки происхождения. Но в этом случае вы получите дополнительный косвенный доступ.
  • Иногда вы можете избавиться от упаковки в std:: function, просто используя шаблоны.
  • Я не уверен, что значение должно быть shared_ptr. Возможно, вызывающий должен решить это.
  • Вы собираетесь создавать новые блокировки при каждом доступе.

Рассмотрим следующую модификацию:

template<typename F>
lazy(const F& x) :
  thunk_ptr([&x,&this](){
    T val = (*x)();
    thunk_ptr = [val]() { return val; };
    return val;
  })
{}

Или альтернативная реализация может выглядеть так:

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)(); };
};

Ответ 2

Да, у вас есть ленивый. В принципе, просто передайте функцию, которая вычисляет аргумент вместо аргумента. После оценки объект заменяется вычисленным значением. Это в основном это и да, реализовано так, с указателями с подсчетом ссылок, это довольно дорого.

Memoization - это старый термин, который часто означает табуляцию результата функции. Ни один современный язык не делает этого (может быть, PROLOG), это очень дорого. Полная ленивость (никогда не вычисляющая одну вещь дважды) достигается в процессе лямбда-подъема, что является устранением свободных переменных (ставя их в качестве аргументов). При полностью ленивом подъеме лямбда это максимальные свободные выражения, которые поднимаются (скажем, х свободен, поэтому замена sqrt x заменяется новым аргументом sqrtx). Существует также так называемое оптимальное сокращение.

Я не думаю, что есть другие способы сделать это. Почему это происходит намного быстрее на ленивом функциональном языке, таком как Haskell? Ну, в основном, никаких ссылок не подсчитанных указателей, тогда есть анализ строгости (строгий - это противоположность ленивому), что позволяет компилятору заранее знать, что некоторые вещи лучше оцениваются строго, unboxing значения, которые оцениваются строго и имеют известный тип машины... не говоря уже о других типичных оптимизациях функционального программирования... Но в сущности, если вы посмотрите на реализацию машины сокращения графа, если вы посмотрите, как развивается стек, вы видите, что в основном вы передаете функции в стеке вместо аргументов, и это в основном это.

Теперь, в этих машинах, node, который вычисляет аргумент, перезаписывается с его значением. Таким образом, вы, возможно, не хотите оптимизировать, но это невозможно в безопасном для текста контексте.

Предположим, что все ваши "узлы", где находятся подклассы главного суперкласса, называемые "node", которые имели только виртуальную функцию, которая вычисляла значение... затем она могла быть "перезаписана" другой, которая вернет уже вычисленные стоимость. Эта вещь, с указателями функций, - это то, почему они говорят, что машина STG из Haskell является "безразличной" (Spineless Tagless G-Machine), поскольку они не маркируют элементы данных, вместо этого они используют указатель функции, который либо вычисляет, либо возвращает значение.

Я не думаю, что это может быть сделано не так эффективно в С++, как в Haskell... если мы не начнем думать о реализации С++ совершенно по-другому (может и должно быть сделано). Мы привыкли к таким сложным прологам и эпилогам, а также к сложным соглашениям о вызовах и т.д. Вызов функции слишком бюрократичен в C/С++.

Теперь, прочитав книгу, когда вы чувствуете себя ленивой, определенно "Реализация языков функционального программирования" Саймона Пейтона-Джонса. Тем не менее, современная реализация описана в свободно доступной статье "Внедрение функциональных языков на биржевом оборудовании: The Spineless Tagless G-machine", который очень приятно читать об оптимизации реализации, но другой - тот, который нужно читать, чтобы понять основы.

Ответ 3

Вот еще один аспект лени, который мне нужен.

// 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;
};

С использованием примерно так:

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 );
}

и позже:

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;

Ответ 4

Библиотека Boost Phoenix реализует Lazyness, среди других тонкостей FP, но я не использовал себя. Я не уверен, насколько хорошо он играет с С++ 11, или, возможно, сделал это, по крайней мере, частично стандартом 2011 года.

http://www.boost.org/doc/libs/1_43_0/libs/spirit/phoenix/doc/html/index.html

Ответ 5

В моей реализации класса Lazy я пошел немного по-другому - функция лямбда не возвращает значение, она принимает его как параметр. Это помогает достичь определенных преимуществ:

  • Сохраненное время при вызове move constructor для инкапсулированного типа (когда функция инициализации возвращает результат).
  • Конструктор копирования и оператор присваивания для инкапсулированного типа не требуются (только если вы хотите сделать это для Lazy).

Кроме того, эта версия должна быть потокобезопасной (пожалуйста, исправьте меня, если я сделал что-то не так). Одно требование, которое все еще остается - конструктор по умолчанию.

#pragma once
#include <mutex>
#include <atomic>
#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
};

Пример использования:

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;