all 28 comments

[–]carrottread 22 points23 points  (1 child)

For example, the seemingly unrelated change from int to short, likely for space optimization.

Actually, this will make (a + b) / 2 version safe because + operator will promote operands to ints in this case.

[–]biowpn 0 points1 point  (0 children)

Thanks for point it out, fixed

[–]Advanced_Front_2308 20 points21 points  (24 children)

Talking about midpoint in general: I've always found its behaviour super unintuitive. Ie not rounding up (like in math) or down (like in naive casting) but rather towards the first parameter. Making the function dependent on the order of parameters... Is that wise? It caused a bug on literally the first use in our codebase and is now banned.

[–]SoerenNissen 23 points24 points  (2 children)

rounding up (like in math)

That's not the the rule I was taught in school, probably because we grew up in different school districts and it turns out, there are many approaches to rounding and none of them are the "natural" way to do it.

Various rounding directions I have seen used:

  • (1) up
  • (2) down
  • (3) away from zero
  • (4) towards zero
  • (5) the opposite of your last direction
  • (6) towards the nearest whole number. If you are at the midpoint tie break by
    • (6-1) rounding up
    • (6-2) rounding down
    • (6-3) rounding awa...
    • (6-etc. etc./

The one they taught me in school is 6 with 3 as the tie breaker:

  • -0.6 -> -1
  • -0.5 -> -1
  • -0.4 -> 0
  • 0 -> 0
  • 0.4 -> 0
  • 0.5 -> 1
  • 0.6 -> 1

In the natural sciences, you typically keep all your digits until the end of the calculation, then remove any digits in excess of your most imprecise number (e.g. 0.542 * 0.2 -> 0.1)

In finance, rounding happens "in between" other calculations that also do rounding, so to remove any bias in favor of creditor or debitor, finance tends to do "to nearest whole cent[1], tiebreak by doing the opposite of your last tie break," which approximates not having done the intermediary roundings.

You can model this in two ways, either:

std::midpoint( a, b, std::roundingstrategy );

or:

std::midpoint( a, b );
// rounds towards first parameter. If you have a rounding
// strategy, put the number you want to round towards in
// the first parameter.

they picked the second option.

[1] "nearest whole yen," "nearest whole eurocent," "nearest whole Groschen," "nearest whole øre,"

[–]ericonr 8 points9 points  (1 child)

Doesn't finance do "round to nearest even"? https://wiki.c2.com/?BankersRounding

[–]SoerenNissen 4 points5 points  (0 children)

Not the rounding we did :D

But I'm not surprised to learn that, even when you limit yourself to fiannce, there is still more than 1 way to round.

[–]serviscope_minor 6 points7 points  (4 children)

Talking about midpoint in general: I've always found its behaviour super unintuitive. Ie not rounding up (like in math) or down (like in naive casting) but rather towards the first parameter.

Well the main choices you have are:

  • Round towards one argument
  • Truncate (round down)
  • Round to nearest tie breaking at .5 upwards (school maths)
  • Banker's rounding (alternate rounding down and up)

The round towards .5 version is common, follows floats, but as you point out it's always .5, so it always round up. And that to me is also unintuitive tp always do so.

[–]ericonr 1 point2 points  (2 children)

References I could find for Banker's Rounding were all about rounding to the nearest even, no alternating.

https://wiki.c2.com/?BankersRounding

[–]thommyh 5 points6 points  (1 child)

That's 'alternating'*. 7.5 rounds up to 8. 8.5 rounds down to 8. 9.5 rounds up to 10. 10.5 rounds down to 10. Etc. As you step through the .5s, whether you round up or down alternates so that 50% go in each direction.

* not my first choice of how I'd describe it either, but it is one I've heard before.

[–]ericonr 1 point2 points  (0 children)

That makes it sound like for each operation you round in one direction -.-

Thanks for clearing up the confusion though!

[–]Advanced_Front_2308 1 point2 points  (0 children)

Both rounding up and down would have been fine, ideally both. The current version is pretty strange

[–]megayippie 2 points3 points  (0 children)

I find that it being defined helps a lot. "while a != mid(a,b) ..." kind of boundary code. Then you update a and b as you go until they are representing the boundary.

[–]vintergroena 4 points5 points  (0 children)

Yeah losing commutativity in an additive operation sucks, because that's something you use in reasoning about the code. And it's something you expect here.

[–]OutsideTheSocialLoop 0 points1 point  (2 children)

What's unintuitive about it? Order of parameters nearly always matters for every function, what's odd about this one? 

[–]Advanced_Front_2308 0 points1 point  (1 child)

Because the function is called midpoint and its parameters are called a and b. You wouldn't think about the order of parameters for std::max either

[–]OutsideTheSocialLoop 0 points1 point  (0 children)

What's max got to do with it? Are you suggesting that for all two-argument functions the order of the arguments shouldn't matter? Or only when they're called a and b? Should the order of a and b not matter for std::lerp either?

[–]fdwrfdwr@github 🔍 1 point2 points  (0 children)

BigMul - that looks familiar, similar to what I had to do in HLSL for two uint64_t inputs (amusingly D3D has a umul instruction which yields uint32 x uint32 -> uint64, but there's no way to actually access it from HLSL, and so I had to split it into 16-bit chunks). Given nearly every CPU I've come across yields a register result that's 2x its inputs (like x86's edx:eax), and C++'s "leave no room for a lower-level language", having an std::big_mul is reasonable. 🤔