【问题标题】:Handling references to `expressions` in a `vector_binary_operation` class without unnecessary copies在 `vector_binary_operation` 类中处理对 `expressions` 的引用,而不需要不必要的副本
【发布时间】:2016-02-25 12:27:16
【问题描述】:

我正在尝试实现一个简单的vector 类,它使用expression templates 以避免低效地实现诸如vector w = x + y + z 之类的表达式(实现将首先生成一个临时向量来保存x + y,然后生成另一个 vector 并添加 z 的元素):

namespace math
{
    template<class E>
    class expression
    {
    public:
        auto size() const {
            return static_cast<E const&>(*this).size();
        }

        auto operator[](std::size_t i) const
        {
            if (i >= size())
                throw std::length_error("");
            return static_cast<E const&>(*this)[i];
        }

        operator E&() { return static_cast<E&>(*this); }
        operator E const&() const { return static_cast<E const&>(*this); }
    }; // class expression

    template<typename T, class Allocator = std::allocator<T>>
    class vector
        : public expression<vector<T>>
    {
    private:
        using data_type = std::vector<T, Allocator>;
        data_type m_data;

    public:
        using value_type = T;
        using allocator_type = Allocator;
        using size_type = typename data_type::size_type;
        using difference_type = typename data_type::difference_type;
        using reference = typename data_type::reference;
        using const_reference = typename data_type::const_reference;
        using pointer = typename data_type::pointer ;
        using const_pointer = typename data_type::const_pointer;

        vector(size_type d)
            : m_data(d)
        { }
        vector(std::initializer_list<value_type> init)
            : m_data(init)
        { }
        template<class E>
        vector(expression<E> const& expression)
            : m_data(expression.size())
        {
            for (size_type i = 0; i < expression.size(); ++i)
                m_data[i] = expression[i];
        }

        size_type size() const {
            return m_data.size();
        }
        
        value_type  operator[](size_type i) const { return m_data[i]; }
        value_type& operator[](size_type i)       { return m_data[i]; };
    }; // class vector

    namespace detail
    {
        template<typename T>
        class scalar
            : public expression<scalar<T>>
        {
        public:
            using value_type = T;
            using allocator_type = std::allocator<void>;
            using size_type = typename std::allocator<T>::size_type;
            using difference_type = typename std::allocator<T>::difference_type;
            using reference = typename std::allocator<T>::reference;
            using const_reference = typename std::allocator<T>::const_reference;
            using pointer = typename std::allocator<T>::pointer;
            using const_pointer = typename std::allocator<T>::const_pointer;

            scalar(value_type value)
                : m_value(value)
            { }

            size_type size() const {
                return 0;
            }

            operator value_type&() { return static_cast<value_type&>(*this); }
            operator value_type const&() const { return static_cast<value_type const&>(*this); }

            value_type  operator[](size_type i) const { return m_value; }
            value_type& operator[](size_type i) { return m_value; }

        private:
            value_type m_value;
        }; // class scalar

        template<class>
        struct is_scalar : std::false_type { };

        template<class T>
        struct is_scalar<scalar<T>> : std::true_type { };
    } // namespace detail

    template<class E1, class E2, class BinaryOperation>
    class vector_binary_operation
        : public expression<vector_binary_operation<E1, E2, BinaryOperation>>
    {
    public:
        using value_type = decltype(BinaryOperation()(typename E1::value_type(), typename E2::value_type()));
        using allocator_type = std::conditional_t<
            detail::is_scalar<E1>::value,
            typename E2::allocator_type::template rebind<value_type>::other,
            typename E1::allocator_type::template rebind<value_type>::other>;

    private:
        using vector_type = vector<value_type, allocator_type>;

    public:
        using size_type = typename vector_type::size_type;
        using difference_type = typename vector_type::difference_type;
        using reference = typename vector_type::reference;
        using const_reference = typename vector_type::const_reference;
        using pointer = typename vector_type::pointer;
        using const_pointer = typename vector_type::const_pointer;

        vector_binary_operation(expression<E1> const& e1, expression<E2> const& e2, BinaryOperation op)
            : m_e1(e1), m_e2(e2),
              m_op(op)
        {
            if (e1.size() > 0 && e2.size() > 0 && !(e1.size() == e2.size()))
                throw std::logic_error("");
        }

        size_type size() const {
            return m_e1.size(); // == m_e2.size()
        }

        value_type operator[](size_type i) const {
            return m_op(m_e1[i], m_e2[i]);
        }

    private:
        E1 m_e1;
        E2 m_e2;
        //E1 const& m_e1;
        //E2 const& m_e2;
        BinaryOperation m_op;
    }; // class vector_binary_operation

    template<class E1, class E2>
    vector_binary_operation<E1, E2, std::plus<>>
    operator+(expression<E1> const& e1, expression<E2> const& e2) {
        return{ e1, e2, std::plus<>() };
    }
    template<class E1, class E2>
    vector_binary_operation<E1, E2, std::minus<>>
    operator-(expression<E1> const& e1, expression<E2> const& e2) {
        return{ e1, e2, std::minus<>() };
    }
    template<class E1, class E2>
    vector_binary_operation<E1, E2, std::multiplies<>>
    operator*(expression<E1> const& e1, expression<E2> const& e2) {
        return{ e1, e2, std::multiplies<>() };
    }
    template<class E1, class E2>
    vector_binary_operation<E1, E2, std::divides<>>
    operator/(expression<E1> const& e1, expression<E2> const& e2) {
        return{ e1, e2, std::divides<>() };
    }

    template<class E, typename T>
    vector_binary_operation<E, detail::scalar<T>, std::divides<>>
    operator/(expression<E> const& expr, T val) {
        return{ expr, detail::scalar<T>(val), std::divides<>() };
    }
    template<class E, typename T>
    vector_binary_operation<E, detail::scalar<T>, std::multiplies<>>
    operator*(T val, expression<E> const& expr) {
        return{ expr, detail::scalar<T>(val), std::multiplies<>() };
    }
    template<class E, typename T>
    vector_binary_operation<E, detail::scalar<T>, std::multiplies<>>
    operator*(expression<E> const& expr, T val) {
        return{ expr, detail::scalar<T>(val), std::multiplies<>() };
    }
} // namespace math

我不知道我需要如何处理vector_binary_operation 中的expression 成员变量。将它们声明为 const 引用是有意义的,因为代码的重点是避免不必要的复制。但是,如果我们写sum = a + b + c,我们最终将保留对临时a + b 的引用。执行sum[0] 将在该临时调用operator()[0]。但是该对象在上一行之后被删除了。

我该怎么办?

【问题讨论】:

  • std::vector 具有移动语义,因此不应有任何临时副本。另见:stackoverflow.com/questions/10720122/…
  • @NathanOliver 你误解了我的意思。我的意思是m_e1m_e2 可能包含一些vectors 的副本。
  • C++11 引入了移动语义,它允许您从临时对象中窃取内容。简化示例:coliru。现在,如果您想以不同的方式处理临时对象和引用(即,获得临时对象的所有权,但通过 const 引用持有非临时对象),这可能会带来一些模板乐趣 :)
  • @melak47 以不同方式处理临时对象和引用似乎是个好主意,但我不知道该怎么做。
  • 我想知道它是否真的有必要 - 一旦创建了你可以操纵你的向量操作吗?如果没有,我认为复制那些懒惰评估的操作没有太大的危害,然后你不需要特殊处理

标签: c++ templates c++11 c++14


【解决方案1】:

很久以前,我在我的表达式模板代码中面临着类似的决定。因此,如何存储包装数组的选择至关重要,应谨慎进行。但是让我们假设您已经解决了 references:也就是说,如果您有类似的代码

vector a;
vector b;
auto c = a + b;

ab 由 const-reference 存储。这意味着只要您使用c,原始向量就必须保持活动状态。 (其他选择是按值存储,这可能很昂贵,或者按(包装)共享指针存储,即使ab 离开了它们的范围但也可以使用c有调用开销。)

好的,但是现在当您确定引用时,您显然希望避免引用临时文件。为此,C++ 移动语义已经提供了你需要的任何东西:

template<typename E1, typename E2>
auto operator+(E1&& e1, E2&& e2)
{
     using value_type = decltype(e1[0] + e2[0]);
     return vector_binary_operation<E1, E2, std::plus<value_type> >
                   {std::forward<E1>(e1), std::forward<E2>(e2), std::plus<value_type>{}};
}

您的代码的不同之处在于使用的 rvalue 引用 E1&amp;&amp;E2&amp;&amp; 的类型推导。它们的目的是指示函数是否获得了有效且命名的对象或临时对象。根据此信息,您可以在此函数中选择如何将传递的引用存储在返回的表达式类中。


例如,如果你现在添加两个向量

auto c = a + b;

E1E2 将被推导出为vector&amp;。相反,如果你有

auto d = (a + b) + b;

E1vector_binary_operation&lt;vector&amp;, vector&amp;, std::plus&lt;&gt; &gt; -- 这里没有引用 -- 而 E2 仍然是 vector&amp;

【讨论】:

  • 如果我没有大错特错,我不需要修改任何代码,除了独立的操作符,对吧?我不确定我是否喜欢 ab 只要 c 还活着就需要活着,但如果我们不想使用 @ 似乎我们无能为力987654343@或副本,对吧?
  • @davidhigh 也是,我相信你可以在operator+ 中使用std::plus&lt;&gt; 并让它为你做类型推断。
  • @melak47 是的,我们可以做到。 std::plus&lt;&gt; 使用相同的技术来推断类型。
  • 是的,std::plus&lt;&gt; 似乎是自 C++14 以来的新事物,如 here 所述。
  • @0xbadf00d: 再说一遍,imo 最重要的选择在前面,即如何存储其他表达式。在我的代码中,我更喜欢通过包装共享指针来做到这一点,所以你没有生命周期问题:表达式a + b 将使两个加法都保持活动状态。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-28
  • 1970-01-01
  • 1970-01-01
  • 2017-10-11
  • 1970-01-01
相关资源
最近更新 更多