Класс C ++ с вложенными шаблонами выражений - PullRequest
0 голосов
/ 30 ноября 2018

Я хочу определить класс, называемый здесь Nested, который будет содержать два или более (один здесь) члена данных, которые поддерживают арифметические операции с использованием шаблонов выражений, например std::valarray.Для самого этого класса я определяю его собственные шаблоны выражений и хочу «переслать» арифметические операции членам.

Ниже приведен минимальный (не) рабочий пример:

#include <iostream>
#include <valarray>

template <typename E>
struct NestedExpr {
    operator const E& () const {
        return *static_cast<const E*>(this);
    }
};

template <typename A>
class Nested : public NestedExpr <Nested<A>>{
private:
    A a;
public:
    Nested(const A& _a) : a(_a) {}

    template <typename E>
    inline Nested<A>& operator = (const NestedExpr<E>& _expr) {
        const E& expr(_expr);
        a = expr.get_a();
        return *this;
    }

    inline       A& get_a()       { return a; }
    inline const A& get_a() const { return a; }
};

// ================================================================= //
template <typename ARG, typename S>
class NestedMul : public NestedExpr<NestedMul<ARG, S>> {
public:
    const ARG& arg;
    const S      s;
    NestedMul(const ARG& _arg, S _s) : arg(_arg), s(_s) {}
    inline auto get_a() const { return arg.get_a() * s; };
};

template< typename ARG, typename S>
inline NestedMul<ARG, S> operator * (S s, const NestedExpr<ARG>& arg) {
    return {arg, s};
}

// ================================================================= //
template <typename ARG1, typename ARG2>
class NestedAdd : public NestedExpr<NestedAdd<ARG1, ARG2>> {
public:
    const ARG1& arg1;
    const ARG2& arg2;
    NestedAdd(const ARG1& _arg1, const ARG2& _arg2)
        : arg1(_arg1), arg2(_arg2) {}
    inline auto get_a() const { return arg1.get_a() + arg2.get_a(); };
};

template<typename ARG1, typename ARG2>
inline NestedAdd<ARG1, ARG2> 
operator + (const NestedExpr<ARG1>& arg1, const NestedExpr<ARG2>& arg2) {
    return {arg1, arg2};
}

int main () {
    std::valarray<double> x1 = {4.0};
    std::valarray<double> x2 = {3.0};
    std::valarray<double> x3 = {0.0};
    std::valarray<double> x4 = {0.0};

    auto a = Nested<std::valarray<double>>(x1);
    auto b = Nested<std::valarray<double>>(x2);
    auto c = Nested<std::valarray<double>>(x3);

    // this returns 21
    c  = 2*a  + 3*b;
    std::cout << c.get_a()[0] << std::endl;

    // works as expected, returns 17
    x4 = 2*x1 + 3*x2;
    std::cout <<        x4[0] << std::endl;
}

Вывод этой программы:

21
17

, т. Е. Пересылка выражения вниз члену, по-видимому, не дает ожидаемого результата, полученного непосредственно от использования valarrays.

Любая помощь здесьценится.

1 Ответ

0 голосов
/ 01 декабря 2018

В приведенном ниже определении функции:

inline auto get_a() const { return arg.get_a() * s; };

ваше ожидаемое поведение таково, что auto выводит std::valarray<double>, то есть тип результата умножения std::valarray<double> и int, которыйновый объект, который уже хранит значения, умноженные на целое число.

Вот как определяется operator* [valarray.binary] / p2 :

template <class T>
valarray<T> operator*(const valarray<T>&,
                      const typename valarray<T>::value_type&);

Однакоесть следующий абзац в стандарте [valarray.syn] / p3 :

Любая функция, возвращающая valarray<T>, может вернуть объект другого типа, при условии, что все функции-константы valarray<T> также применимы к этому типу.Этот возвращаемый тип не должен добавлять более двух уровней вложенности шаблонов к наиболее глубоко вложенному типу аргумента.

Этот тип должен быть преобразован в std::valarray<double>, но сам по себе, в целях оптимизации, может не представлятьфактический результат до того, как произойдет это преобразование.

То есть вот фактический тип, выведенный для GCC auto:

std::_Expr<std::__detail::_BinClos<std::__multiplies
                                 , std::_ValArray
                                 , std::_Constant, double, double>, double>

и вот что использует Clang:

std::__1::__val_expr<std::__1::_BinaryOp<std::__1::multiplies<double>, 
                     std::__1::valarray<double>, std::__1::__scalar_expr<double> > >

Другими словами, вы возвращаете по значению объект, который, вероятно, откладывает фактические вычисления.Чтобы сделать это, эти промежуточные объекты должны как-то хранить отложенные подвыражения.

Осматривая реализацию GCC libstdc ++, можно найти следующее представление:

template <class _Oper, class _FirstArg, class _SecondArg>
class _BinBase
{
public:
    typedef typename _FirstArg::value_type _Vt;
    typedef typename __fun<_Oper, _Vt>::result_type value_type;

    _BinBase(const _FirstArg& __e1, const _SecondArg& __e2)
    : _M_expr1(__e1), _M_expr2(__e2) {}

    // [...]

private:
    const _FirstArg& _M_expr1;
    const _SecondArg& _M_expr2;
};

Обратите внимание, что подвыражения хранятся как ссылка .Это означает, что в определении get_a():

return arg1.get_a() + arg2.get_a();

_M_expr1 и _M_expr2 связаны с временными объектами:

  • arg1.get_a()
  • arg2.get_a()

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

value_type operator[](size_t __i) const
{
    return _Oper()(_M_expr1[__i], _M_expr2[__i]);
}

Быстрое решение состоит в том, чтобы использовать следующий тип возврата:

std::decay_t<decltype(arg.get_a())> get_a() const { return arg.get_a() * s; }

Thisбудет рекурсивно гарантировать, что конечный тип результата любой операции будет таким, какой был исходный тип T в Nested<T>, т. е. std::valarray<double>.

DEMO

...