I love C++

Discussion in 'Programming' started by Bobcat00, Nov 27, 2019.

  1. Yes, that is sarcasm. I just spent two days doing something in C++ that would take two minutes in C.

    I need to do calculations with phase angles of electromagnetic signals. In particular, I need to calculate the difference in phase angle between two samples (i.e., subtract two angles). Now we treat phase angles as being in degrees from 0 to 360. Yes, you could argue we should use radians, or make the range positive and negative, but it really doesn't matter. I need to subtract angles.

    The problem, of course, is the wrap-around from 360 to 0. So 359 - 1 should equal -2, not 358. There are various ways to handle this, but the fancy way is to take the sine of the difference, the cosine of the difference, then the arc tangent (atan2) of those to get the phase angle difference. And that's what I've done in C in the past, making a subtract_angles function which takes two floats and returns a float with the difference. It's simple and it works.

    But now I'm using C++, so hey, I can do this in a class and overload operator-. So I start making my Angle class, with constructor, copy constructor, copy assignment operator, and destructor. (This is already 10 times the size of my one-line subtract_angles C function.) Then I turn my attention to overloading operator-. So I look online for the proper syntax, and I find four different web pages telling me four different ways to do it. Oh Gawd. So I start to write that, then I realize I also need to overload operator-=. More looking online, and I discover that you make a member function for operator-=, then make a non-member function for operator- which uses operator-=.

    So I get that working, and I can write something like this:
    Code (Text):
    Angle a(10.0f);
    Angle b = a - 20.0f;
    But wait! I can't write Angle b = 20.0f - a. Aw, jeez. More looking online, and I see there's a different way to write the operator- overload which allows a numeric literal to be used on the left hand side. (The Angle class constructor kindly conjures up an Angle object from the literal.)

    OK, now we're cooking with gas. Let's try b = 20.0f + a. That doesn't work, because there's no operator+ defined. Wonderful. So then I declare a conversion function to go from Angle to float, so the 'a' gets converted to a float, does a regular floating point add, then the constructor turns it back into an Angle object. Oh, but now my operator- overload doesn't work, because it doesn't know whether to use the normal subtract or my overloaded subtract. But this can be handled by making the constructor for Angle 'explicit'. But that breaks the add and subtract functions. So then I need to do stuff like b = Angle(20.0f) - a and b = Angle(20.0f + a) because otherwise it doesn't use the Angle constructor to make an Angle object from a numeric literal.

    And this is supposed to be easier than a one-line C function? What a load of crap.

    I haven't decided which way to go with this yet. With the class and the overloading and the conversion and the explicit, it's not clear to the user what is actually going on and which versions of which functions are being used. Do my co-workers really want to trust that I haven't made some mistake in defining this? The good news is I'll be leaving in a couple months and someone else will have to deal with this. I can't believe anyone actually thinks C++ is a good idea.

    60 years of high-level language development and we end up with C++?
     
    #1 Bobcat00, Nov 27, 2019
    Last edited: Nov 27, 2019
    • Agree Agree x 1
    • Funny Funny x 1
    • Optimistic Optimistic x 1
  2. C++ isn’t made to be a very high-level language. It compiles to Windows ASM. There’s no automatic memory management, you have to do that yourself (Java does this with “garbage collection”) and in general it’s very low level. It has its good uses, and because it compiles to ASM it’s very fast. The syntax is ugly, but it’s a pretty good language, depending what it’s being used for.
     
    • Agree Agree x 1
  3. I really love how the high complexity of making a function call with two arguments can be completely removed by scattering inconsistent magic-inducing nuggets of syntactic poop all over in a HiGhLy CoHeSiVe ClAsS, so that you can make an implicit function call instead of an explicit one by using an infix binary operator. You still need to make a function call or two to a constructor function sometimes, but at least each call only takes a single argument! I think that's a win!

    C++ tends to give you all the tools to solve your problem in ten different ways, and all of them are the wrong way. The least wrong way depends on the sub religion of the readers of the code you write. I studied with a guy who wrote a compiler for a subset of Java purely using the template engine. Compiling usually took around 6 GB memory. He also had a brief dialog with Bjarne Stroustrup about it, and he said "don't do that".

    If the task at hand is picking flowers, Java is the adult with a pair of rounded tip scissors who takes your hand, leads you to the flower bed, hands you the scissors, watches your every move as you cut the stem, then takes back the scissors and leads you back to where you came from. C++ is the dirty, greasy redneck who gives you a chainsaw, spits in your face, laughs, and yells "GOOD LUCK, CITY BOY! HOOOOO!".
     
    • Winner Winner x 2
    • Agree Agree x 1
  4. You did unneccessary complex design decisions with topics you didn't know completely yet vs. simply writing one or two normal member functions.
     
    #4 Janmm14, Nov 27, 2019
    Last edited: Nov 27, 2019
    • Agree Agree x 3
  5. First of all, I agree with your sentiment - operator overloading in C++ is an absolute mess and extremely overused.

    That said, I don't think you took the right approach here. -2deg is the same as 358deg modulo 360. Hence, all the trigonometric functions treat them the same. What is your use case where -2deg and 358deg need to be treated differently? I see only three situations where this could be troublesome: If you're printing this number to the user, you can do the conversion in your printing function, which is where that logic belongs. If you need the absolute distance (so std::abs(angle)), then do this in your custom abs(angle) function. If you're scared of integer overflows, do %= 360 here and then. Those are really all the situations where I could see -2 and 358 behave differently - what's yours?

    No, 60 years of high-level language development and we end up with Rust. C++ came a few million years before that
     
  6. I'll try to clarify. I'm taking the difference between two phase angles. In my example, one angle is 359 and the other is 1. So the difference between the two is 2 degrees. Another way to look at it is the shortest distance around the circle between the two phase angles.

    The result is a difference, not an angle in itself. This number is then used in additional calculations.

    And not that it really matters, but going counterclockwise yields a positive result, and clockwise a negative result.

    (Want more fun? Figure out how to average angles. The average of 359 and 1 is 0, not 180.)

    More examples:
    50 degrees - 350 degrees is 60 degrees.
    50 degrees - 10 degrees is 40 degrees.
     
    #6 Bobcat00, Nov 27, 2019
    Last edited: Nov 27, 2019
    • Winner Winner x 1
  7. I think you're missing the point...

    In general, the goal of higher abstraction levels is to hide complexity and trivialize repetitive tasks into languages and libraries. Operator overloading is perhaps the mother of all bikesheds in this regard. People will go into endless discussions about just how stupid it is to not have it, because "look at how simple this little example becomes!". The immediate argument is the one I just presented; it hides the complexity of doing arithmetic operations on complex types and trivializes the underlying method calls via arithmetic operators. The problem is that the abstraction is as leaky as a bucket turned upside down. There is no general formal specification for what "+" means outside of the pervasive, basic arithmetic operations you see everywhere, which means you're forced to rely on documentation or code inspection (this goes without saying for stuff like normal function calls, but do you really want to be unsure or skeptical about what a plus operator in your code does?). Just take Java's string concatenation as an example. You're used to it now, but what sense does it really make to add two strings? Operator overloading is an awkward language construct, not a complex one. Any complexity comes from the abstraction being leaky in both directions; implementations are unreliable at best, and you're forced to make decisions for the compiler about the AST to make things "implicit enough", but then everything ends up bleeding anyway.

    The first point of the post is that C++ is a hot mess in the world of high-level languages. A lot of people disagree because they are now familiar with it (don't mistake familiarity for simplicity), but that doesn't change the fact that there are languages out there that are much, much more successful at hiding complexity and trivializing repetitive tasks. C++ is a broken promise. Something that is more obvious nowadays where we can see a clear rise in popularity of other "medium-level" programming languages like Go and Rust.

    The second point of the post is the dangers of over-engineering. You spend lengthy periods of time trying to iron out a kink that isn't hurting anyone, and in the process, you make dents in a lot of other places (or people). Over-engineering often leads to overfitting, a state in which any new requirement to the software will require you to pull everything up by its roots and re-implement it because it just won't fit. If you instead KISS (Keep It Simple and Stupid), you usually end up getting from start to finish more quickly and with software that is actually soft. The reason it's called software is because it is meant to be malleable unlike hardware (fun fact: the term "firmware" comes from being the glue between soft- and hardware, thus not quite hard, not quite soft, but firm). The big challenge here is convincing people that "clever code is bad, dumb code is good", and the fact that writing dumb code is actually really, really difficult.

    There is definitely a point to be made about wielding unfamiliar tools, but this is not the time nor the place for it. There is very clearly no lack of understanding of the topic, but rather with the implementation details and how boisterous these are. Operator overloading as a language construct originates from cases exactly like this one, and it still falls short. Even the poster child of the concept, vector arithmetic, is broken and ambiguous - how would you implement multiplication?
     
    #7 garbagemule, Nov 27, 2019
    Last edited: Nov 28, 2019
  8. Ha, ha. That's great. I was thinking of running my above experience past some of the C++ weenies at work, but I don't know if I want to subject myself to more of this misery.

    Java is not really acceptable in our application, because if the garbage collector runs for 20 msec, it could result in someone dying. That's why most of our code does not do memory allocation except at program initialization. But doing OOP in C++, or just using the STL, pretty much throws that out the window.
     
  9. Part of this looks to me like there is no IDE for C++ nearly as feature-picked as IntelliJ Idea for Java.
     
  10. We use Eclipse. And it's targeted for a TI TMS320C6678 DSP.

    But again, the issue is the poor language constructs and syntax for operator overloading in C++. Plus the idea that you need to have a class to represent something like angles which have a non-standard meaning when it comes to subtraction. The same effect can be done with a single function.
     
  11. I understand, but instead of making sure that every subtraction result is in [-180, 180), you can make sure that's the case every time you call a function that's sensitive to that, in that function (such as user output, abs or avg).

    Either way operator overloading is overkill for this, unless this is part of a public library. You can just do the same thing as in C and define a function to do the job. Unless you want the syntax, but in that case you'll have to pay the cost.

    Rust?

    That said, Rust's operator overloading is still somewhat inconvenient. Both languages require you to very carefully define what's the right way to do stuff. They could have a default implementation of += akin to a += b :== a = a + b, which might work well on integers, but is a performance nightmare on some larger types because it does the addition, and then copies the entire value back, instead of modifying in-place. Same thing why you have like 15 different ways to overload assignment in C++ - you have the standard assignment operator, copy constructors, move assignments, and so on. And then all those method's arguments could be const, or the instance could be const, which you need to deal with as well. Each of those fits one very particular use case, and the compiler (or programmer) will use what's applicable for maximum performance. Is it worth the performance in your case, I don't know. Is it worth the performance in some cases, definitely.
     
    #11 StillNoNumber, Nov 28, 2019
    Last edited: Nov 28, 2019
  12. Looks like something was cut-off by mistake?
     
  13. Thanks, fixed. At first I wanted to start my post with that, ended up deciding to start it with what it starts now. It somehow remained.

    What I was trying to say is: What you're doing is not subtraction, it's subtraction where the result is in [-180, 180). I don't think overloading is the right way to go in that case, and it's certainly a lot more awkward as you have discovered.
     
  14. Well, there's IntelliJ CLion which I find to work just as well for C++ as Idea does for Java. It basically feels like it's exactly the same program (which it very well might be).
    I used to use Eclipse for C/C++ and switching to CLion simply makes it a billion times faster and easier to write code; it helped me to love C++ even more than I already did.
     
  15. Just an FYI, it is. Most jetbrains ide's are based off intellij, just with the java removed, and another language being specialised in.
     
  16. Offtopic, but to elaborate further, some of their IDEs are just IntelliJ Community, but with a different set of plugins enabled/disabled to achieve its goal: "(...) products such as WebStorm and DataGrip are based on the IntelliJ IDEA Community Edition, but with a different set of plugins included and excluding other default plugins." (https://www.jetbrains.org/intellij/sdk/docs/intro/intellij_platform.html)
     
  17. Looking at it further, it appears that subtraction will work when the frame of reference is -180 to +180, and the result is normalized to be in that range by adding or subtracting 360. I'm guessing that's because the range is symmetrical around zero. This was not intuitively obvious to me.

    But the overloading is a mess, regardless. So the question now is: Do I make an angle class just for the sake of making a class? Or just leave them as floats and include subtract and normalize functions as would be done in C?
     
    • Like Like x 1
  18. I'd go with the class. That way you can ensure that normal floats, that are not angles, cannot accidentally be subtracted using your angle method or can be normalized. If you just make them as two functions, it could be possible to accidentally get angles and normal floats messed up and accidentally call angle-specific functions with normal floats, or vice versa (especially if you work with both in the same piece of code). If you make an Angle class and then make functions that only work with those (or provide these functions on the class itself), you can't accidentally mess that up. The only downside would be that, if you need to get your angle as a float, or vice versa, you'd need to convert them, although I'd say this class-based approach, despite of that, is still preferable.

    I must note that my experience with C++ is limited and I have almost no experience with C nor have I done much work with non-OOP languages, so our views on this might differ quite a bit.
     
    • Like Like x 1
  19. Note that you can make a class yet still not overload subtraction, making the subtract function a member method. That way, you get the benefits of Stef's method (type safety, can't accidentally do normal subtraction, etc.) while not having to deal with operator overloading.
     
  20. Since the operator overloading venture turned sour, maybe a bit of reductio ad absurdum in the other direction can help put things into perspective.

    You could choose to represent the angles as bit strings instead of using floats. Doing so makes everything needlessly complex, and while arguably not the best, it is an option. Simple addition or subtraction becomes a huge mess, because it is no longer a simple, well-defined operation, but a complex endeavor with lots of room for errors. Now do the same for all primitive types. Integers, floating point numbers, even strings. Given any variable, it is no longer immediately obvious what data type it holds. This is obviously ridiculous. But imagine if there was a primitive angle type in C++ that automatically handled over- and underflow around the circle, had a well-defined "angle between points on circumference" operation, etc. Would it not be ridiculous, then, to use floats instead?

    Object-oriented programming's shtick is collocation of state and related behavior. Given an object, you can find out what operations it supports by looking at its member functions. This creates a sort of conceptual boundary around the state it holds. You can choose to capitalize on that boundary. Some people prefer to collocate very specific and "powerful" behavior with a particular type of state (e.g. Martin Fowler*), while others prefer to completely dislocate behavior from state (e.g. Justin Searls**).

    In this case, you're working in a class-based, object-oriented, statically typed language. This means that classes are first-class citizens, there is great support for collocation of state and related behavior, and you get to offload some sanity checks to the compiler as per @Stef's point. You could even go as far as provide methods for both degrees and radians, so e.g. getValueInDegrees() and getValueInRadians(), or maybe getValue(AngleUnit) where AngleUnit is an enum of DEGREES and RADIANS. This would make the unit explicit and "configurable", removing any assumptions, and reducing the risk of something like NASA's disaster in '99.

    If you want an example of something along the same lines, maybe the Vector class in the Bukkit project could be worth a look. Note that mutability is especially relevant for "value types" like this. This implementation allows you to change the value of a given Vector instance, but other implementations always instantiate new objects for referential integrity (it's basically pointers vs. values).

    And just to circle back to operator overloading...

    You could easily extend my points here to a "completeness argument". If you have the types, why not just take the extra step and implement the behavior with operator overloading? My personal take is that those operators belong with the primitive types. Their behavior is well-defined and battle-hardened across many different programming languages and paradigms. Keeping them out of the picture creates a very clear line between primitive types, which are acted upon with operators, and complex types, which are operated upon with functions. I find that simple rules like this help people write dumb code, which is far easier to read, and thus much more "team friendly" than clever code.

    ---

    * Check out how Fowler uses the Order and Warehouse types together in the first code snippet here. Notice how the fill() method exists on Order, which means the Order class is responsible for taking stuff out of the Warehouse, rather than the Warehouse being responsible for filling an Order, or a different class that works on both types.

    ** Searls coined the term Discovery Testing, which leads to architecture with "value objects" with no behavior (structs in C), pure functions that have no dependencies, produce no side-effects, and act on values, and finally "collaborator objects" that just make very simple function calls and contain no real logic. He has a 4-part YouTube series on it, it's pretty interesting. He also made an example Bukkit plugin following this paradigm.
     
    • Creative Creative x 1