2022-10-22

This is not a job for the algorithms header, after all.

As Mark points out below, the whole premise of this post is based on having misread the documentation. My initial expectations were more or less correct, and my effort to be a careful documentation-reading programmer back fired. Well, $#!+! There is a probably a lesson there.

Watch this spot for further posts to push my shame down past the point where most poeple keep scrolling. Or something.


I was stymied today.

You see the code I was working on feaures a type, call it KeyThing, that includes a double weight member intended to be used in forming averages over some information accessible using the rest of KeyThing.

Because the absolute scale of the data accessed wih the key matters, averaging is proceeded by a normalizing step: the sum of the weights must add up to unity. Which means you have to, well, accumulate the weights. Right?

So, being a good little programmer, I reach for the algorithm header and start by writing const auto sum = std::accumulate(collecion.begin(), collection.end(), 0.0, [](){}); only to pause briefly because I need to give the empty lambda the right signature. I'm thinking that it must take one double and one iterator that I can use to get access to the weight, and I just need to look up the order they come in.

C++ Reference quickly disabused me of that notion. The binary operation in std::accumulate expects to recieve two values that are of (or are implicily converable to) the type of the initializing argument (here double).

I can't use a lambda like [](const container::const_iterator it, double sum) { return sum + it->value; } wih either order for the arguments. Perhaps the symmeric-arguments version is more in keeping with the principle of least astonishment and generates clearer code in the easy majoriy of cases, but this is also a lost opportunity for more generaltiy. Now, obviousness and clarity are significant virtues in an API, so I won't claim it represents a bug, but it sure tripped me up this time.

So I wrote double sum = 0.0; for (const auto & key : container ) { sum += key.weight; } and moved on. Because while I could write nsc::accumulate which uses my version of the signaure that would be less clear.

3 comments:

  1. The binary function can take different types. It expects the first argument to be the type of the result (double) and the second to be the type that the iterator dereferences to (KeyThing). Here's a way to use std::accumulate to get what you want: https://godbolt.org/z/dTMqbs6KM. All functions (I think) from algorithm that take a function argument expects the argument to operate on the contained types, not the iterators.

    ReplyDelete
    Replies
    1. Completely correct. And thank you even if I feel a bit or a burke just now.

      Delete
    2. You're welcome. I had my moment when I wrote a program that took 8 hours to calculate an answer. When I mentioned the running time, another guy in the lab said, "Haven't you ever heard of a fast fourier transform?" After a bit of wikipedia and downloading FFTW, my program then took 2 seconds to calculate the answer. A 14,000X speedup is impressive, right?

      Delete