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

Как объединить ленивую оценку с авто в С++

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

Я хочу сделать что-то подобное, но я также хочу использовать auto, что невозможно с armadillo (или собственным).

Я немного оглядывался, и этот ответ содержит то, что я считаю типичным способом реализации этого: qaru.site/info/113202/...

Проблема с этим подходом заключается в том, что когда вы пишете

auto C = A+B;

вы получаете C, который является matrix_add, а не matrix. Даже если matrix_add ведет себя так же, как и matrix, тот факт, что matrix_add содержит ссылки на A и B, неудобно переносить. Например.

auto A = matrix(2,2,{0,1,0,1});
auto B = matrix(2,2,{1,0,1,0});
auto C = A+B;
C.printmatrix(); // 1,1 ; 1,1

но

auto A = matrix(2,2,{0,1,0,1});
auto B = matrix(2,2,{1,0,1,0});
auto C = A+B;
A(0,0) = 1;
C.printmatrix(); // 2,1 ; 1,1

что противоречит интуиции. Поскольку математически интуитивное поведение - это то, чего я хочу достичь, это проблема.

Еще хуже то, что я делаю

auto sumMatrices(const matrix& A, const matrix& B)
{
    return A+B;
}

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

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

template<class T>
class maybe_reference
{
public:
    maybe_reference(const T& t):
        ptr_(std::make_unique<T>(t)),
        t_(*ptr_)
    {}
    maybe_reference(std::reference_wrapper<const T> t): 
        t_(t.get())
    {}

    const T& get(){return t_;}
private:
    unique_ptr<T> ptr_;
    const T& t_;
}

Это может быть не реализовано точно таким образом, но общая идея состоит в том, чтобы иметь два конструктора, которые можно четко отличить, чтобы гарантировать, что get() возвращает либо объект, на который ссылается, либо тот, который находится в unique_ptr.

Изменено matrix_add:

class matrix_add {
public:
    friend matrix_add operator+(const matrix& A, const matrix& B);

    matrix_add(matrix_add&& other): A_(other.A_.get()), B_(other.B_.get()){}
private:
    matrix_add(const matrix& A, const matrix& B): A_(std::ref(A)), B_(std::ref(B)){}

    maybe_reference<matrix> A_;
    maybe_reference<matrix> B_;
};

Я забыл все части, которые делают matrix_add, как a matrix. Идея состоит в том, чтобы объект ссылался на внешние объекты A & B, если он был сконструирован с помощью A + B, но когда он сконструирован для перемещения, он будет иметь копии.

Мой вопрос в основном: работает ли это?

Я думал, что конструктор move может быть удален в некоторых или во всех случаях, что может быть разрушительным.

Кроме того, есть ли альтернатива для достижения того же? Я искал, но кажется, что для линейной алгебры, по крайней мере, она либо ленивая, либо авто.

EDIT: благодаря тому, что нам напомнили о термине "шаблоны выражений", мой поиск в google был намного более плодотворным. Я нашел этот reddit-post: https://www.reddit.com/r/cpp/comments/4puabu/news_about_operator_auto/
и справочные документы, которые позволяют специфицировать "отливки" на авто. Это будет особенностью, которая действительно сделает всю эту работу.

4b9b3361

Ответ 1

Вы можете написать функцию шаблона evaluate, которая по умолчанию является NOP, а затем при необходимости перегружать.

#include <utility>
#include <type_traits>

struct matrix {};
struct matrix_add {
  matrix operator()() const;
};

matrix_add operator + (matrix const& a, matrix const& b);


template<class T> decltype(auto) evaluate(T&& val) { return std::forward<T>(val); }
matrix evaluate(matrix_add const& lazy) { return lazy(); }
matrix evaluate(matrix_add & lazy) { return lazy(); }
matrix evaluate(matrix_add && lazy) { return lazy(); }



int main()
{
  auto a = matrix();
  auto b = matrix();

  auto c = evaluate(a + b);
  auto d = evaluate(1 + 2);

  static_assert(std::is_same<decltype(c), matrix>::value, "");
  static_assert(std::is_same<decltype(d), int>::value, "");

}

Ответ 2

Я определяю новый оператор: eager_eval, например:

namespace lazy {
  template<class T>
  void eager_eval(T const volatile&)=delete;

  template<class T>
  struct expression {
    template<class D,
      std::enable_if_t<std::is_base_of<expression, std::decay_t<D>>{}, int> =0
    >
    friend T eager_eval( D&& d ) { return std::forward<D>(d); }
  };
}

Всякий раз, когда вы хотите, чтобы что-то оценивалось с нетерпением, определите eager_eval в своем пространстве имен или выведите его из lazy::lazy_expression<target_type>.

Итак, мы модифицируем ваш matrix_add to (A) из него с помощью ленивого типа, который вам нужен, и (B) имеем оператор matrix:

struct matrix_add:
  lazy::expression<matrix>
{
  matrix_add(matrix const& a, matrix const& b) : a(a), b(b) { }

  operator matrix() && { // rvalue ref qualified as it should be.
    matrix result;
    // Do the addition.
    return result;
  }
private:
  matrix const& a, b;
};

и теперь каждый может сделать:

auto e = eager_eval( a+b );

и ADL находит правильный тип, чтобы он мог оценить ленивое выражение.

живой пример.

Можно, опционально, реализовать по умолчанию eager_eval, который возвращает свой аргумент:

  template<class T, class...Ts>
  T eager_eval(T&& t, Ts&&...)  { return std::forward<T>(t); }

затем

using lazy::eager_eval;
auto x = eager_eval( 1+2 );

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

Пакет в lazy::eager_eval выше означает, что он имеет самый низкий приоритет в качестве перегрузки.

Ответ 3

Я думаю, ваша основная проблема заключается в том, что ленивая оценка не очень хорошо сочетается с изменяющимся состоянием. Я вижу два возможных маршрута:

  • Сделайте свои матрицы неизменяемыми. Если вы "модифицируете" матрицу, вы фактически создаете копию с включенным изменением, оригинал остается нетронутым. Это работает хорошо семантически (любая математика работает точно так же, как вы ожидаете, что она делает), однако при невысоких затратах времени выполнения, если вы задаете значение своих матриц по значению, это может привести к невыносимым издержкам.

    Это позволяет вашей реализации matrix_add молча заменить себя объектом matrix при его оценке, гарантируя, что каждая оценка выполняется не чаще одного раза.

  • Сделать ваши функции явными. Не создавать объекты matrix_add, которые действуют так, как если бы они были самими матрицами, но создавать объекты matrix_function, которые работают с некоторыми входными матрицами, чтобы получить некоторые результат. Это позволяет вам явно выполнять оценку, где вы сочтете нужным, и повторно использовать функции, которые вы определяете. Однако этот подход приведет к значительному усложнению кода.

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

Ответ 4

с выводом аргумента шаблона класса С++ 17, вы можете написать

struct matrix_expr_foo {};
struct matrix_expr_bar {};

template< typename L, typename R >
struct matrix_add {
    // ...
};

matrix_add<matrix_expr_foo,matrix_expr_bar> operator + (matrix_expr_foo const& a, matrix_expr_bar const& b);

template< typename T >
struct expr {
    expr( T const& expr ){
        // evaluate expr ( to be stored in an appropriate member )
    }
    // ...
};

int main()
{
  auto a = matrix_expr_foo();
  auto b = matrix_expr_bar();
  expr c = a + b;

  /* different naming ?
  auto_ c = a + b;
  ...
  */
}

где expr подразумевается как auto для шаблонов выражений...