Вопрос

C ++ не имеет встроенной поддержки отложенного вычисления (в отличие от Haskell).

Мне интересно, возможно ли разумным образом реализовать отложенную оценку в C ++.Если да, то как бы вы это сделали?

Редактировать:Мне нравится ответ Конрада Рудольфа.

Мне интересно, возможно ли реализовать это более общим способом, например, с помощью параметризованного класса lazy, который по сути работает для T так же, как matrix_add работает для matrix.

Любая операция над T вернет вместо этого lazy .Единственная проблема заключается в хранении аргументов и кода операции внутри самого 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 и <=> являются двумерными матрицами, и что разыменование выполняется в нотации Фортрана, то есть в приведенном выше примере один элемент вычисляется из матричной суммы. Конечно, расточительно добавлять целые матрицы. <=> на помощь:

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

Других примеров предостаточно. Я только что вспомнил, что реализовал нечто связанное не так давно. По сути, мне пришлось реализовать строковый класс, который должен придерживаться фиксированного, предварительно определенного интерфейса. Тем не менее, мой конкретный строковый класс имел дело с огромными строками, которые фактически не хранились в памяти. Обычно пользователь просто получает доступ к небольшим подстрокам из исходной строки, используя функцию <=>. Я перегрузил эту функцию для моего строкового типа, чтобы он возвращал прокси, который содержал ссылку на мою строку вместе с желаемой начальной и конечной позицией. Только когда эта подстрока фактически использовалась, он запрашивал API C для получения этой части строки.

Другие советы

Boost.Lambda - это очень хорошо, но Boost.Proto - это именно то, что вы ищете. У него уже есть перегрузки всех операторов C ++, которые по умолчанию выполняют свою обычную функцию при вызове proto::eval(), но могут быть изменены.

То, что уже объяснил Конрад, может быть использовано для поддержки вложенных вызовов операторов, все выполняются лениво. В примере Конрада у него есть объект выражения, который может хранить ровно два аргумента, ровно для двух операндов одной операции. Проблема в том, что он будет выполнять только одно подвыражение лениво, что хорошо объясняет концепцию ленивого вычисления, выраженную в простых терминах, но существенно не повышает производительность. Другой пример также хорошо показывает, как можно применить 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. Это могла быть большая матрица со многими полями. Но в то время, когда нужен результат, мы вычисляем его лениво .

Мне нечего добавить к сообщению Конрада, но вы можете посмотреть на 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;
     }
};

Здесь мы имеем влияние глобальной переменной, которая лениво оценивается в момент первого использования.

По запросу в вопросе.
И крадет дизайн Конрада Рудольфа и расширяет его.

Ленивый объект:

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 , по лямбда-выражениям.

В C ++ 11 ленивая оценка, подобная ответу hiapay, может быть достигнута с помощью std :: shared_future. Вы все еще должны инкапсулировать вычисления в лямбдах, но запоминание позаботится о:

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 в качестве источника нашего вдохновения - он ленив до мозга костей.Кроме того, давайте иметь в виду, как Linq в C # использует перечислители монадическим (тьфу - вот это слово, извините) способом.И последнее, что не менее важно, давайте иметь в виду, что сопрограммы должны предоставлять программистам.А именно, разделение вычислительных этапов (например,производителя-потребителя) друг от друга.И давайте попробуем подумать о том, как сопрограммы соотносятся с отложенной оценкой.

Все вышесказанное, по-видимому, каким-то образом связано.

Далее давайте попробуем извлечь наше личное определение того, к чему сводится понятие "ленивый".

Одна из интерпретаций такова:Мы хотим изложить наши вычисления составным способом, до того, как выполняю его.Некоторые из тех частей, которые мы используем для составления нашего полного решения, вполне могут опираться на огромные (иногда бесконечные) источники данных, при этом наши полные вычисления также приводят либо к конечному, либо к бесконечному результату.

Давайте перейдем к конкретике и к какому-нибудь коду.Для этого нам нужен пример!Здесь я выбираю "проблему" 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

Функция Хаскелла cycle создает бесконечный список (ленивый, конечно!) из конечного списка, просто повторяя значения в конечном списке вечно.В нетерпеливом стиле программирования написание чего-то подобного вызвало бы тревогу (переполнение памяти, бесконечные циклы!).Но не так на ленивом языке.Хитрость в том, что отложенные списки вычисляются не сразу.Может быть, никогда.Обычно только в той мере, в какой этого требует последующий код.

Третья строка в where блок выше создает еще один ленивый!!список, посредством объединения бесконечных списков fizz и buzz с помощью рецепта single two elements "объединить строковый элемент из любого входного списка в одну строку".Опять же, если бы это требовалось немедленно оценить, нам пришлось бы ждать, пока на нашем компьютере закончатся ресурсы.

В 4-й строке мы создаем кортежи членов конечного отложенного списка [1..n] с нашим бесконечным ленивым списком fizzbuzz.Результат по-прежнему остается ленивым.

Даже в основной массе нашего fb таким образом, нет необходимости горячиться.Вся функция возвращает список с решением, которое само по себе - опять же - лениво.С таким же успехом вы могли бы подумать о результате fb 50 как вычисление, которое вы можете (частично) оценить позже.Или комбинируйте с другими материалами, что приведет к еще большей (ленивой) оценке.

Итак, чтобы начать работу с нашей версией "fizzbuzz" на C ++, нам нужно подумать о способах объединения частичных шагов наших вычислений в более крупные биты вычислений, при этом каждый извлекает данные из предыдущих шагов по мере необходимости.

Вы можете ознакомиться с полной историей в моя суть.

Вот основные идеи, лежащие в основе кода:

Заимствуя из C # и Linq, мы "изобретаем" универсальный тип с сохранением состояния Enumerator, который содержит
- Текущее значение частичного вычисления
- Состояние частичного вычисления (чтобы мы могли создавать последующие значения)
- Рабочая функция, которая выдает следующее состояние, следующее значение и bool, который указывает, есть ли еще данные или если перечисление подошло к концу.

Для того, чтобы уметь сочинять Enumerator<T,S> инстанция посредством силы . (точка), этот класс также содержит функции, заимствованные из классов типов Haskell, таких как Functor и Applicative.

Рабочая функция для enumerator всегда имеет вид: 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, нам нужно создать рабочую функцию, иметь начальное состояние и создать экземпляр 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 элементов!

Теперь все, чего не хватает для нашего "вау" опыта, - это посмотреть, как мы можем создавать счетчики.Возвращаясь к Haskells cycle функция, которая в некотором роде классная.Как бы это выглядело в нашем мире 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());
}

Он принимает перечислитель в качестве входных данных и возвращает перечислитель.Локальная (лямбда) функция 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 # enumerators и Linq.Кстати, там может быть сходство с pythons itertools.Я думаю, что они придерживались аналогичного подхода.

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

И каким был бы этот ответ без окончательной версии 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, который делает не что иное, как разыменование указателя для получения значения непосредственно перед тем, когда оно действительно необходимо. Доступ к ленивому типу осуществляется атомарно, поэтому он полностью потокобезопасен.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top