Yes, the * operator can be ambiguous in the context of classic vector math (although that is just a matter of documentation), but not so much with SIMD vectors, audio vectors, etc.
Again:
a) vec4 = (vec1 - vec2) * 0.5 + vec3 * 0.3;
or
b) vec4 = plus(mul(minus(vec1, vec2), 0.5), mul(vec3, 0.3));
Which one is more readable? That's pretty much the perfect use case for operator overloading.
auto t = minus(vec1, vec2); mul_by(t, 0.5/0.3); add(t, vec3); mul_by(t, 0.3); v4 = std::move(t);
However, if the operands are small (e.g. 2/3/4 element vectors are very common), then "unnecessary copies" or move semantics don't come into play at all. These are value types and the compiler would boil them down to the same assembly as the code you post above. Many modern C++ codebases in scientific computing, rendering, or the game industry make use of vector classes with operator overloading, with no performance drawbacks whatsoever; however, code is much more readable, as it matches actual mathematical notation.
> Many modern C++ codebases in scientific computing, rendering, or the game industry make use of vector classes with operator overloading, with no performance drawbacks whatsoever
I guess these people are all not writing "serious code" :-p
Oh please, because you know exactly which kind of code I write? I'm pretty sure that with glm::vec3 the compiler can optimize this just fine. Also, "vec" could really be anything, it is just a placeholder.
That being said, if you need to break up your statements, you can do so with operators:
auto t = vec1 - vec2;
t *= 0.5/0.3;
t += vec3;
t *= 0.3;
Personally, I find this much more readable. But hey, apparently there are people who really prefer free functions. I accept that.And just for the record, I'm very glad Erin Catto decided to use operator overloading in his code. It made it much easier for me to read and understand what the code was doing as opposed to it being overly verbose and noisy.
[0]: https://github.com/erincatto/box2d/blob/main/src/collision/b...
Of course, the compiler or an advanced IDE can know what your code means. If all your identifiers were random permutations of l and I: lIllI1lI, your IDE would not mind either, but the code would be horrific, don't you agree? The point of the OP is that overloaded operators (and functions) make it harder to reason about the code for a human that reads it. At least for some people. At the end, everything is "just" syntactic sugar, but it makes a significant difference.
Python's 'decimal' module uses overloaded operators so you can do things like:
from decimal import Decimal as D
tax_rate = D('0.0765')
subtotal = 0
for item in purchase:
subtotal += item.price * item.count # assume price is a Decimal
taxes = (subtotal * tax_rate).quantize(D('0.00'))
total = subtotal + taxes
Plus, there's support for different rounding modes and precision. In Python's case, something like "a / b" will look to a thread-specific context which specifies the appropriate settings: >>> import decimal
>>> from decimal import localcontext, Decimal as D
>>> D(1) / D(8)
Decimal('0.125')
>>> with localcontext(prec=2):
... D(1) / D(8)
...
Decimal('0.12')
>>> with localcontext(prec=2, rounding=decimal.ROUND_CEILING):
... D(1) / D(8)
...
Decimal('0.13')
Laws can specify which settings to use, for examples, https://www.law.cornell.edu/cfr/text/40/1065.20 includes "Use the following rounding convention, which is consistent with ASTM E29 and NIST SP 811", (1) If the first (left-most) digit to be removed is less than five, remove all the appropriate digits without changing the digits that remain. For example, 3.141593 rounded to the second decimal place is 3.14.
(2) If the first digit to be removed is greater than five, remove all the appropriate digits and increase the lowest-value remaining digit by one. For example, 3.141593 rounded to the fourth decimal place is 3.1416.
... (I've left out some lines)
and from https://www.law.cornell.edu/cfr/text/7/1005.83 : (3) Divide the result in paragraph (a)(2) of this section by 5.5, and round
down to three decimal places to compute the fuel cost adjustment factor;
(4) Add the result in paragraph (a)(3) of this section to $1.91;
(5) Divide the result in paragraph (a)(4) of this section by 480;
(6) Round the result in paragraph (a)(5) of this section down to five decimal
places to compute the mileage rate.
There's probably laws which require multiple and different rounding modes in the calculation.This means simply doing all of the calculations in scaled bigints or as fractions won't really work.
Now of course, you could indeed handle all of this with prefix functions and with explicit context in the function call, but it's going to be more verbose, and obscure the calculation you want to do. I mean, it's not seriously worse. Compare:
with localcontext(prec=3, rounding=decimal.ROUND_DOWN):
line3 = line2 / D("5.5")
line4 = line3 + D("1.91")
line5 = line4 / 480
line6 = line5.quantize(D('.00001'), rounding=decimal.ROUND_DOWN)
vs. some function-based API with overloaded parameter types: line3 = decimal_div(line2, D("5.5"), prec=3, rounding=decimal.ROUND_DOWN)
line4 = decimal_add(line3, D("1.91"))
line5 = decimal_div(line4, 480)
line6 = decimal_quantize(line5, D('.00001'), rounding=decimal.ROUND_DOWN)
But it is worse. I also originally made a typo in the function-based API for line5 where I used "decimal_add" instead of "decimal_div" - the symbols "/" and "+" stand out more, and are less likely to be copy&pasted/auto-completed incorrectly.If overloaded parameters - "spooky action at a distance vibes" - also aren't allowed, then this becomes more rather more complicated.