12

Consider the following MCVE, where I have two value arrays where w is two times v (try it out here):

#include <valarray>

using namespace std;

int main() {
  valarray<int> v { 1, 2, 3 };

  for ([[maybe_unused]] auto x : v) {} // Ok

  auto w = v * 2;     // Leads to failure in loop below
  //valarray<int> w = v * 2; // Works
  //auto w = v*=2;      // Works
  //auto w = v; w *= 2; // Works

  for ([[maybe_unused]] auto x : w) {} // Failure here
}

This example fails to compile with clang and gcc at the last loop with (gcc output here):

error: no matching function for call to 'begin(std::_Expr<std::__detail::_BinClos<std::__multiplies, std::_ValArray, std::_Constant, int, int>, int>&)'

The source of the problem seems to be the decuced type of v * 2 (I assume that because explicitly writing down the type works, so some implicit conversion seems to be taking place).

Looking at the reference notes, it seems that operator* may return something different than std::valarray<T>. I don't understand the reason for this but more puzzling is that the same seem to apply to operator*=, except that here my auto assignment works. I would expect the return values of operator*= and operator* to be the same here (delta the reference).

So my questions are:

  • Is this an implementation issue/bug? Or am I missing something?
  • What's the rationale behind the reference notes (e.g. why can the operators return something different that may not work with std::begin/std::end)?

(Note: I tagged this question c++11, but it seems to apply to all versions up to 17 as well)

andreee
  • 4,459
  • 22
  • 42

1 Answers1

13

There is a trick called expression templates that permit efficiencies in compound expressions, but break horribly with use of auto.

Change this:

auto w = v * 2;

to this:

std::valarray<int> w = v * 2;

and your code works.


To see why we want to use expression templates, try this:

std::valarray<int> a={1,2,3},b{4,5,6},c={2,4,8};
std::valarray<int> r = (a+b*2)*c;

here the expression templates avoid creating a temporary valarray a+b*2 or b*2, but instead pass the entire expression down, and construct r with element-wise operations.

No 3-element valarray temporaries are created in (a+b*2)*c -- just a series of objects that describe the expression structure and arguments. When assigned to an actual valarray the expression is then evaluated on an element-by-element basis.

But auto doesn't convert to valarray; it just stores the expression template object. So your code breaks.

I don't know which versions of the standard permit this or not; regardless, some valarray implementations use this, and it adds a lot of efficiency. Without it, valarray frankly sucks.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • It's a pretty interesting situation actually: http://eel.is/c++draft/valarray.syn#4 – chris May 20 '19 at 14:23
  • 1
    I would add that `std::valarray` was created long before C++11 so there was no `auto` and no range loops. So now this optimization is a problem. IMO this should be fixed by adding `begin` and `end` to this expression templates. – Marek R May 20 '19 at 14:37
  • 2
    @marek Maybe not, and not easy; begin and end that return real useful non-input iterators requires, prior to Rangesv3, a backing container. Maybe Rangesv3 can find a good solution here. – Yakk - Adam Nevraumont May 20 '19 at 14:39
  • That's a nice explanation! I still wonder why _a)_ operators `*` and `*=` behave differently, [see here](https://godbolt.org/z/gp0p6A) (P.S. I also tried `libc++` in clang). Why is there such inconsistency? And _b)_ why is the expression template not converted back to `valarray` "in-place" somehow? It seems to be done in `operator *=`, after all... – andreee May 20 '19 at 15:08
  • 1
    `*=` stores result on the left agreement so new object is not created. `*` creates new objects so it needs allocation and writing to memory, that is why proxy object is created to avoid unneeded memory allocation and writes on this piece of memory. – Marek R May 20 '19 at 15:16
  • Oh, of course. That's what Adam's answer already suggests. In the `*=` case, we're modifying the original object anyway and only need to return`*this`, while in the `*` case we can avoid a temporary. Understood. – andreee May 20 '19 at 15:19
  • @andreee This might be naive explanation, but if you think about those operations as expressions, they are different. One is taking three operands, and the other just two, including storage. And in the case of non-self assignment operator, you avoid creating a temporary storage if you introduce "*replacement type*" that the note in [here](https://en.cppreference.com/w/cpp/numeric/valarray/operator_arith3) talks about. Unary version does not have such note, but you are guaranteed that storage exists. The "replacement type" is the expression type that can get evaluated to `valarray` (et al.) – luk32 May 20 '19 at 15:21
  • I guess if it had `const` begin and end members (which [it doesn't](http://eel.is/c++draft/template.valarray.overview)) then [this wouldn't be a problem](http://eel.is/c++draft/numarray#valarray.syn-3)). Ouch. – Lightness Races in Orbit May 31 '19 at 12:37
  • @light to iterate it would have to allocate, due to how iterators are specified in C++17 and before. Ivan.sort of see the utility, at the same time not. – Yakk - Adam Nevraumont May 31 '19 at 12:52
  • Why would it have to allocate? I can iterate `std::array` just fine – Lightness Races in Orbit May 31 '19 at 13:10
  • @LightnessRacesinOrbit Because `valarray` is variable sized, and the intermediate objects just carry references to other arrays and don't have storage. In order to iterate over the values produced, it needs backing storage ("real" iterating over generators is illegal; only input iterators can be generators, and `begin` on a valarray isn't an input iterator), and no backing storage exists, so once `begin` is called backing storage has to be brought into existence and persist long enough to last until the iteration is done. Try writing it. It is a pain. – Yakk - Adam Nevraumont May 31 '19 at 15:54