【问题标题】:Efficient vector operators in C++ / References to temporary objectsC++ 中的高效向量运算符 / 对临时对象的引用
【发布时间】:2015-07-06 07:32:11
【问题描述】:

我正在尝试编写一个 C++ 矢量类,它存储数据数组并允许逐个元素执行数学运算。我想以这样一种方式实现这一点,即表达式 a = b + c + d 应该只遍历所有元素一次,并直接将和 b[i] + c[i] + d[i] 写入 a[i] 而不创建中间向量。

我写的是这样的:

template<class T, int N>
class VectorExpression {
  public:
    virtual T operator[] (int i) const = 0;

    virtual ~VectorExpression() {}
}

template<class T, int N>
class MyVector : public VectorExpression<T, N> {
    T data[N];

  public:
    T& operator[] (int i) { return data[i]; }
    T& const operator[] (int i) const { return data[i]; }

    MyVector<T,N>& operator=(const VectorExpression<T,N> &rhs) {
      for (int i = 0; i < N; ++i)
        data[i] = rhs[i];

      return *this;
    }
}

template<class T, int N>
class VectorSum : public VectorExpression<T, N> {
    VectorExpression<T,N> &a, &b;

  public:
    VectorSum(VectorExpression<T,N> &aa, VectorExpression<T,N> &bb)
    : a(aa), b(bb) {}

    T operator[] (int i) const { return a[i] + b[i]; }
}

template<class T, int N>
VectorSum<T,N> operator+(const VectorExpression<T,N> &a, 
        const VectorExpression<T,N> &b) 
{
  return VectorSum<T,N>(a, b);
}

int main() {
  MyVector<double,10> a, b, c, d;

  // Initialize b, c, d here

  a = b + c + d;

  return 0;
}

这个功能可能是由 valarray 类提供的,但那是因为我试图将它简化为一个最小的示例。

我将operator[] 设为虚拟,因为这允许嵌套所有类型的表达式(例如a = !(-b*c + d)),前提是我将定义所有类似于VectorSum 的运算符和相应的类。

我使用引用是因为普通变量不是多态的,并且指针不适用于运算符重载。

现在我的问题是:

  • a = b + c + d;语句中,会创建两个临时的VectorSum&lt;double,10&gt;对象,分别存储b + c(b+c) + d。它们的寿命是否足以使多态行为发挥作用?更具体地说,(b+c) + d 将存储对b + c 的引用,但是当调用operator= 时该对象是否仍然存在?根据this post,所有临时对象都应该存在,直到operator= 返回,但这是否也适用于旧版本的C++?

  • 如果不是,那么这是如何完成的?我看到的唯一选择是使用new 分配VectorSum 对象,通过引用返回它们,然后在operator= 函数中删除它们,但这似乎有点麻烦,而且效率可能低得多。我也不确定它是否总是安全的。

  • (小问题)可以在MyVector中用T&amp; const覆盖VectorExpression::operator[]的返回类型T吗?

编辑

我在 operator+ 中有错误的参数类型:将它们从 VectorSum 更改为 VectorExpression

【问题讨论】:

  • 这不能回答您的问题,我也没有深入研究您的代码,但我不确定这是实现对您的总和进行有效后期评估的方法。你考虑过使用expression templates 吗?
  • 据我了解,您所指的网站与我所做的或多或少相同,只是使用模板而不是继承。我没有想到,所以谢谢。不过,我不太确定哪个选项更好;他们的方法不需要虚拟类,但它确实需要更多的类(特别是如果你在模板实例化之后计算它们)并且所有这些类的临时对象都必须实例化。
  • 您可能会感兴趣:stackoverflow.com/q/11809052/1116364
  • 谢谢!这样代码变得相当复杂,但我现在看到,使用表达式模板,您可以在运行时完全消除所有临时和函数调用,这在使用继承和虚函数时是不可能的。在大多数情况下,这可能使它成为比我更好的解决方案。

标签: c++ reference polymorphism


【解决方案1】:

这就是我想出的:

#include <iostream>
#include <initializer_list>
#include <algorithm>


template<class T, int N>
class VectorExpression {
public:
    virtual T operator[] (int i) = 0;
    virtual const T operator[] (int i) const = 0;

    virtual ~VectorExpression() {}
};


template<class T, int N>
class MyVector : public VectorExpression<T, N> {
  T data[N];

public:
  MyVector() {
    // initialize zero
    std::fill(std::begin(data), std::end(data), T());
  }

  MyVector(const std::initializer_list<T>& values) {
    // initialize from array initializer_list
    std::copy(std::begin(values), std::end(values), data);
  }

  MyVector(const VectorExpression<T,N>& rhs) { 
    for (int i = 0; i < N; ++i)
      data[i] = rhs[i];
  }

  MyVector<T,N>& operator=(const VectorExpression<T,N>& rhs) {
    for (int i = 0; i < N; ++i)
      data[i] = rhs[i];

    return *this;
  }

  T operator[] (int i) { return data[i]; }
  const T operator[] (int i) const { return data[i]; }

  friend std::ostream& operator<<(std::ostream& stream, MyVector& obj) { 
    stream << "[";
    for (int i = 0; i < N; ++i) { 
      stream << obj.data[i] << ", ";
    }
    stream << "]";

    return stream;
  }
};

template<class T, int N>
class VectorSum : public VectorExpression<T, N> {
  const MyVector<T,N> &a, &b;

public:
  VectorSum(const MyVector<T,N>& aa, const MyVector<T,N>& bb):
    a(aa), b(bb) {
  }

  T operator[] (int i) { return return a[i] + b[i]; }

  const T operator[] (int i) const { return a[i] + b[i]; }
};


template<class T, int N>
MyVector<T,N> operator+(const MyVector<T,N>& a, const MyVector<T,N>& b) {
  return VectorSum<T,N>(a, b);
}


int main() {
  MyVector<double,3> a, b({1,2,3}), c({3,4,5}), d({4,5,6});

  a = b + c + d; 

  std::cout << b << std::endl;
  std::cout << c << std::endl;
  std::cout << d << std::endl;
  std::cout << "Result:\n" << a << std::endl;

  return 0;
}

输出:

[1, 2, 3, ]
[3, 4, 5, ]
[4, 5, 6, ]
Result:
[8, 11, 14, ]

我添加了一个 initializer_list (C++11) 构造函数和 ostream 运算符,纯粹是为了方便/说明。

由于您已将 operator[] 定义为按值返回,因此我无法在数据数组中设置项目以进行测试(因为错误:需要左值作为赋值的左操作数);通常这个运算符应该是通过引用 - 但是,在你的情况下, VectorSum::operator[] 将不起作用,因为这会因为返回对临时的引用而导致编译失败。

我还添加了一个复制构造函数,因为 ...

// this calls MyVector's copy constructor when assigned to 'main::a'
template<class T, int N>
MyVector<T,N> operator+(const MyVector<T,N>& a, const MyVector<T,N>& b) {
  return VectorSum<T,N>(a, b);   // implicit MyVector::copy constructor 
}

// this also calls MyVector's copy constructor (unless the copy constructor is defined explicit)
template<class T, int N>
MyVector<T,N> operator+(const MyVector<T,N>& a, const MyVector<T,N>& b) {
  MyVector<T,N> res = VectorSum<T,N>(a, b);
  return res;
}

// but this would call MyVector's assignment operator
template<class T, int N>
MyVector<T,N> operator+(const MyVector<T,N>& a, const MyVector<T,N>& b) {
  MyVector<T,N> res;
  res = VectorSum<T,N>(a, b);
  return res;
}

回答您的问题:

  1. 是的 - 如果您明确定义变量并且 退回那个?临时工的行为是一样的,除了那里 是没有变量声明;
  2. 不适用
  3. 我在上面提到过 - 你不能 由于“返回临时引用”错误而使用引用; 但是,您没有理由将 T& operator[] 添加到 MyVector(即不覆盖)。

编辑:对 cme​​ts 的回答:

  1. 重写时函数规范必须相同,包括返回类型。由于您已在 VectorExpression 中定义了按值返回,因此它必须在 MyVector 中按值返回。如果您尝试将其更改为子类中的引用,您将收到编译错误:指定的返回类型冲突。所以不,你不能用返回 const T& 而不是 T 的版本覆盖 operator const。此外,它 必须 按值返回,因为 MyVectorSum 返回 { a[i] + b[i] } 这将是一个临时的,你不能返回对临时的引用。

  2. 对不起,我的错误,上面已修复。

  3. 因为:

    • MyVector 不是 VectorSum 的子类型 - 编译错误“MyVector”不是从“const VectorSum”派生的
    • 我也尝试过 VectorExpression,但编译错误:'无法分配抽象类型的对象' - 因为它试图按值返回
    • 我选择了 MyVector,因为这是您预期的结果类型。是的,它完成了所有那些 for 循环,但我看不出有什么办法:有三个不同的数组“数据”变量,每个变量都需要迭代才能累积。在代码中的某个时刻,您将不得不执行 for 循环。
  4. 明白,是的,我很困惑。从帖子中删除。

【讨论】:

  • 1) 我没有将MyVector::operator[] 定义为按值返回,因此在我的示例中,您可以将元素放入向量中。我只在VectorExpressionVectorSum 中这样做了,因为您通常不能分配给表达式。他们只有operator[](int) const;在MyVector 中,我添加了一个非覆盖operator[](int)。我的问题是是否可以使用返回 const T&amp; 而不是 T 的版本覆盖 operator[](int) const
  • 2) 我看不出你添加的意义T operator[] (int i) { return 0; }:现在,如果你的 VectorExpression 对象恰好是非常量,它将错误地返回 0。因为你无论如何都没有通过引用返回我不明白你为什么不省略这个方法?
  • 3) 我想知道你为什么将operator+ 的返回类型更改为MyVector&lt;T,N&gt;。我可能弄错了,但这似乎破坏了拥有一个在一个 for 循环中进行评估的高效运算符的全部意义?现在您将评估b + c 并将其存储在MyVector 中,然后创建另一个MyVector 来存储此对象和d 的总和,然后将其复制到a,所以我们有三个用于-现在循环...
  • 4) 成员函数模板是一个成员函数,它本身就是一个模板,即具有除类参数之外的模板参数。当模板类被实例化时,这些函数仍然是模板,因此它们不能是虚拟的。这与模板类的成员函数不同,模板类在实例化时成为常规成员函数。我看不出为什么这些不能是虚拟的。
  • 5) @Richard:回复你的回答 3:我的参数类型错误,确实有VectorSum 没有意义;应该是VectorExpression(现在改了)。但我仍然不明白为什么你改变了返回类型。 VectorSum 不是抽象的,所以这里应该没有任何问题?您不必在任何地方执行 for 循环(复数),只需一个 for 循环就足够了,这就是我想要实现的目标。
【解决方案2】:

一开始我并没有想到这一点,但是拥有一个虚拟的 operator[] 方法可能会通过避免 3 个 for 循环和向量大小的临时对象的中间存储来降低我试图实现的效率。将方法设为虚拟可防止它被内联,这意味着它需要作为函数实际调用每次访问元素时

根据我从对我的问题发表评论的人和 Google 那里获得的链接,我最终得到了以下解决方案,它避免了需要任何虚拟方法。

template<class T, int N, class V>
class VectorExpressionBase {
    V ref;

protected:
    explicit VectorExpressionBase(const V *ref) 
    : ref(const_cast<V*>(ref)) {}

public:
    T  operator[] (int i) const { return ref[i]; }
    T& operator[] (int i)       { return ref[i]; }
}

template<class T, int N>
class VectorExpressionBase<T,N,void> {
    T data[N];

protected:
    explicit VectorExpressionBase(const void*) {
        // Argument is unused but accepted to have uniform
        // calling syntax
    }

public:
    T  operator[] (int i) const { return data[i]; }
    T& operator[] (int i)       { return data[i]; }
}

template<class T, int N, class V>
class VectorExpression : public VectorExpressionBase<T,N,V> {
public:
    template<class V1>
    VectorExpression<T,N,V>& operator= (
        const VectorExpression<T,N,V1> &rhs) 
    {
        for (int i = 0; i < N; ++i)
            data[i] = rhs[i];

        return *this;
    }

    explicit VectorExpression(const V *ref = 0) 
    : VectorExpressionBase<T,N,V>(ref) {}

    // Can define all kinds of operators and functions here such as
    // +=, *=, unary + and -, max(), min(), sin(), cos(), ...
    // They would automatically apply to MyVector objects and to the
    // results of other operators and functions
};

template<class T, int N>
class MyVector : public VectorExpression<T,N,void> {
}

template<class T, int N, class VA, class VB>
class VectorSum {
    VectorExpression<T,N,VA> &a;
    VectorExpression<T,N,VB> &b;

  public:
    VectorSum(VectorExpression<T,N,VA> &aa, VectorExpression<T,N,VB> &bb)
    : a(aa), b(bb) {}

    T operator[] (int i) const { return a[i] + b[i]; }
}

template<class T, int N, class VA, class VB>
VectorExpression<T,N,VectorSum<T,N,VA,VB> >
operator+(const VectorExpression<T,N,VA> &a, 
          const VectorExpression<T,N,VB> &b) 
{
    VectorSum<T,N,VA,VB> sum(a, b);
    return VectorExpression<T,N,VectorSum<T,N,VA,VB> >(sum);
}

VectorExpression 类现在只包装完成工作的类(在本例中为VectorSum。这允许仅为VectorExpression 定义各种函数和运算符,而不必为VectorSum 重载它们, VectorProduct

MyVector 派生自 VectorExpression 的特例,它有一个专门的基类;这并不是真正必要的,但它很好,因为它使为VectorExpression 定义的所有函数和运算符也可用于MyVector。通过使用仅处理存储的简单基类VectorExpressionBase[] 运算符,所有其他运算符和方法都不需要在V = void 的特化中重复。

如果用户想要定义额外的函数和操作符,他们只需要知道类MyVector&lt;T,N&gt;(用于存储数据)和可能的VectorExpression&lt;T,N,V&gt;VectorExpressionBaseVectorSum 这样的类不需要对外界可见。

我发现我最初的解决方案在概念上更加清晰,因为每个类的含义更清晰,并且不需要模板参数V,但是这个更有效,因为它不需要任何虚函数,在某些情况下,这可能会产生很大的不同。

感谢您为我指出正确的链接!

附:当然,大多数/所有这些都不是新的,但我认为总结和解释一下会很好。我希望它可以帮助其他人。

编辑

我将数据成员VectorExpressionBase&lt;T,N,V&gt;::ref 的类型从V&amp; 更改为V。这是必需的,因为在评估 VectorExpression 时,引用指向的临时 V 对象可能不再存在。例如,当operator+函数返回时,临时的VectorSum对象不存在,使得返回的VectorExpression对象无用。

我还用一些构造函数完成了代码并更正了operator+ 函数。

【讨论】:

  • 你的虚函数很可能是编译器的devirtualized,实际上并不是性能问题。
  • 但我假设每个 MyVector 对象仍将包含一个 vtable 指针,因为无法保证 所有 函数都可以总是被去虚拟化?对于使用许多小向量的程序,这可能是一个严重的内存开销。还是我在这里遗漏了什么?
  • 我会忽略它,一旦你的内存用完了(这永远不会发生),我会回到它。如果您现在需要解决方案,我将删除继承,添加一个函数 VectorExpression&lt;T, N&gt; MyVector::get_expression() 并使用 get_expression 实现代数运算符。您将需要一个适当大小的 N 才能看到性能改进,而且对于低 Ns,中间的 MyVector 可能更快。
  • 这些确实是好点:如果N 很低,则中间MyVector 的开销低到零,如果N 很大,则vtable 指针的开销可以忽略不计,所以这可能是一个有点学术问题。我考虑了您的解决方案,但最终提出了上述解决方案,它不需要将VectorExpression 中的每个函数都包装在MyVector 中。代价是它变得更复杂,并且由于所有的模板和中间类,编译速度可能会更慢。
  • @nwp:关于您对内存不足的评论:我从事硬件设计行业(实际上使用的是基于 C++ 构建的 SystemC)。在我们的业务中,通常的规则是,只要您没有耗尽内存或 CPU,就没有理由在实际生产您的芯片之前不运行额外的模拟 :-)
猜你喜欢
  • 2020-01-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-02-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多