Do you ever think of developing in other languages than C++?

DSP, Plugin and Host development discussion.
Post Reply New Topic
RELATED
PRODUCTS

Post

Z1202 wrote: Fri Mar 19, 2021 8:08 pm
0degree wrote: Fri Mar 19, 2021 7:40 pm
Z1202 wrote: Fri Mar 19, 2021 7:23 pm FWIW, IMHO C++ could have fit into one's head if it wasn't for its irregularities and exceptions (such as UB). It is tempting to think of C++ as a high-level assembler (wasn't C initially intended to be like that?), in which case most of the features have easy intuitive understandings... Except that these understandings are no longer valid, thanks to the irregularities and UB.
But that's exactly the state of the c++, isn't it? Most c++ devs will tell you to not use exeptions (making STL unsuable), disable RTTI, UB here, UB there.
Add to it concepts, couroutines and 2000 pages of the language spec (or maybe even more these days?). Surely won't fit into one's hand or even dozen of them :D
I think a half if not more of these 2000 pages are due to irregularities and UB. So the language could still have been saved, if they are removed ;)
I've been playing around with concepts, they are a huge step forward in making metaprogramming way more accessible and getting intelligible error messages ;)
Ok somebody mentioned metaprogramming....(screaming and running away :wheee: )

Post

Let's see: Rust "solves" issues by forbidding the cases that are not trivial to begin with. Nim has a garbage collector. Zig promises to never implement any of the features that make large C++ projects manageable (seriously; I'm not exactly sure why any of these should be taken seriously).

Personally I don't care for a "simple" language. If I wanted a "simple" language I'd be writing C. I want a language that is more powerful than C++ with more features (reflection, better meta-programming, etc), with explicit control over everything, but more mechanisms to hide the complexity under the rug. The only thing C++ needs less is UB.

Post

mystran wrote: Fri Mar 19, 2021 8:36 pm Personally I don't care for a "simple" language. If I wanted a "simple" language I'd be writing C. I want a language that is more powerful than C++ with more features (reflection, better meta-programming, etc), with explicit control over everything, but more mechanisms to hide the complexity under the rug. The only thing C++ needs less is UB.
Off the top of my head, the walls I ran into with C++
  • Reinterpret cast is mostly UB (wasn't it the original intention of such casts in C to access memory with several different interpretations? or was is a common misuse of the language which ultimately stopped working?)
  • union field switching (reading without writing) was a proposed way to circumvent the reinterpret cast's UB (also with more concerns in regards to register optimization) but turned out to be UB as well
  • turning offsetof into a builtin with a drastically reduced functionality (IMHO anything which, by now illegal, macro-based definition of offsetof allowed, should be allowed, otherwise it's too limiting)
  • speaking of offsetof macro-based definition being now illegal, it should be fully legal to formally access any uninitialized memory, as long as no real accesses are being made
  • accessing a base class data member via a pointer to derived class pointing to an object which is actually just of the base class (needed this once, had to find another solution) is UB
  • signed overflow is UB instead of simply being platform-specific (I think it's getting better in C++20, but I'm not sure if it's fully defined now)
  • newer STL features are missing fast access options, which don't do runtime checks (assertions in debug mode are welcome), e.g. std::function or std::variant
  • if constexpr can be used only in function bodies. In particular, it's not possible to have conditionally-present data members in class templates (no_unique_address makes this at least possible, but still unnecessary complicated). It also wasn't possible to have conditionally-present methods (SFINAE can't be used if the method itself is not a template), at least this is solved with constraints in C++
  • lambdas can't be used in unevaluated contexts, at least they plan to allow it for lambdas, but I guess declval is still going to suffer from it
  • Flexible array member is officially UB
  • Arrays cannot have zero size.
  • Two objects of the same class are not allowed to have the same address. I can understand that sometimes it's undesired, but I'd rather allow it by default and have means to forbid it for specific classes.
  • Because of the scanning rules it is much much easier to have in-class inline function bodies in templates than in non-template classes (the former can refer to yet unknown identifiers).
At least for me, it's not just UB. These are the kinds of things I was referring to as irregularities (offsetof works, but not in all intuitively expected situations, if constexpr works, but not in all intuitively expected situations, etc.)

Post

Z1202 wrote: Fri Mar 19, 2021 9:06 pm
  • Reinterpret cast is mostly UB (wasn't it the original intention of such casts in C to access memory with several different interpretations? or was is a common misuse of the language which ultimately stopped working?)
  • union field switching (reading without writing) was a proposed way to circumvent the reinterpret cast's UB (also with more concerns in regards to register optimization) but turned out to be UB as well
With these the compilers are fortunately a bit more sane than what the standard allows. It sucks that you have to rely on UB to get stuff done, but .. practically speaking that UB usually does what you want anyway (or otherwise a ton of legacy C APIs and half of POSIX would break).

I mean you can still legally reintepret by doing a memcpy() but like .. yeah, who does that? :)
  • accessing a base class data member via a pointer to derived class pointing to an object which is actually just of the base class (needed this once, had to find another solution) is UB
This one pretty much has to be either UB or illegal with the way inheritance is normally implemented, because a pointer to a base class doesn't necessarily have the same address as a pointer to a derived class, even if it's the same object. Why? Because if you have several base classes then only one of these can be placed in the beginning of the object, so the field offsets can actually end up being different depending on the actual type of the pointer itself.
  • signed overflow is UB instead of simply being platform-specific (I think it's getting better in C++20, but I'm not sure if it's fully defined now)
This is actually one of those things that sucks either way. From the programmers point of view, having signed overflow be UB is terrible, but from the compiler's point of view, forcing a certain behaviour (even an implementation-defined behaviour) means essentially giving up some common (relatively high-value; that's the trouble, this has more impact that most of the other "UB for performance" cases) optimizations in many cases or forcing complicated additional guards (which are not free either). So it's actually a legit trade-off between clean language semantics and performance; personally I hate it being UB, but it does have some real advantages too.
  • newer STL features are missing fast access options, which don't do runtime checks (assertions in debug mode are welcome), e.g. std::function or std::variant
Hold on. These do type-erasure and can store a bunch of different stuff (which is kinda the whole point they exist in the first place), so they need a run-time dispatch either way, so runtime error check don't really add any extra cost as it's already there either way. I don't see why you'd ever be using std::function or std::variant if you already know the type.. so I don't see a problem here.
  • Arrays cannot have zero size.
  • Two objects of the same class are not allowed to have the same address. I can understand that sometimes it's undesired, but I'd rather allow it by default and have means to forbid it for specific classes.
I believe these two are actually the same thing. The address of an array is the address of the first element and you need a unique first element for every array in order to not have the same address for two arrays.

Post

mystran wrote: Fri Mar 19, 2021 10:02 pm
Z1202 wrote: Fri Mar 19, 2021 9:06 pm
  • Reinterpret cast is mostly UB (wasn't it the original intention of such casts in C to access memory with several different interpretations? or was is a common misuse of the language which ultimately stopped working?)
  • union field switching (reading without writing) was a proposed way to circumvent the reinterpret cast's UB (also with more concerns in regards to register optimization) but turned out to be UB as well
With these the compilers are fortunately a bit more sane than what the standard allows.
At least not the gcc with reinterpret cast
I mean you can still legally reintepret by doing a memcpy() but like .. yeah, who does that? :)
I'm slowly learning to do that
  • accessing a base class data member via a pointer to derived class pointing to an object which is actually just of the base class (needed this once, had to find another solution) is UB
This one pretty much has to be either UB or illegal with the way inheritance is normally implemented, because a pointer to a base class doesn't necessarily have the same address as a pointer to a derived class, even if it's the same object.
So what? I'm having a proper pointer to the derived class in the sense, that if its value has to differ from the base class, then so it does. It is obtained by static casting.
  • signed overflow is UB instead of simply being platform-specific (I think it's getting better in C++20, but I'm not sure if it's fully defined now)
This is actually one of those things that sucks either way. From the programmers point of view, having signed overflow be UB is terrible, but from the compiler's point of view, forcing a certain behaviour (even an implementation-defined behaviour) means essentially giving up some common (relatively high-value; that's the trouble, this has more impact that most of the other "UB for performance" cases) optimizations
Then there could have been some standard intrinsic to bypass that or whatever.
  • newer STL features are missing fast access options, which don't do runtime checks (assertions in debug mode are welcome), e.g. std::function or std::variant
Hold on. These do type-erasure and can store a bunch of different stuff (which is kinda the whole point they exist in the first place), so they need a run-time dispatch either way, so runtime error check don't really add any extra cost as it's already there either way. I don't see why you'd ever be using std::function or std::variant if you already know the type.. so I don't see a problem here.
Unless I'm missing something, std::variant is essentially just a union with some safety wrappers on top and run time information about the type. I would expect to have an option to access the respective union field bypassing the safety check (except for a debug mode assertion). Similarly I'd expect to be able to invoke an std::function pointing to a static member function without any runtime checks (this is what the generated code actually does, except that it's required to throw an exception if the pointer is null, so it has to do a runtime check before the call).

Post

Z1202 wrote: Fri Mar 19, 2021 10:18 pm
This is actually one of those things that sucks either way. From the programmers point of view, having signed overflow be UB is terrible, but from the compiler's point of view, forcing a certain behaviour (even an implementation-defined behaviour) means essentially giving up some common (relatively high-value; that's the trouble, this has more impact that most of the other "UB for performance" cases) optimizations
Then there could have been some standard intrinsic to bypass that or whatever.
There is: cast to unsigned, perform operation, cast back to signed. Addition, subtraction and multiplication result in the same bit-pattern in both unsigned and 2s-complement signed arithmetic and division cannot overflow.
Similarly I'd expect to be able to invoke an std::function pointing to a static member function without any runtime checks (this is what the generated code actually does, except that it's required to throw an exception if the pointer is null, so it has to do a runtime check before the call).
I'm not too familiar with the implementation of std::variant (because I've never found a reason to use it for anything), but at least with std::function you can have a plain function (including any C function or static member function), a "small" lambda that is stored inline (as an optimization, when only a few variables are captured), or you could have a "big" lambda that is allocated in the heap, any object that overrides operator(), bind expressions (well, I guess those are just objects that overload operator() as well), etc.. Plain functions need a function call, lambdas with captures need a method call and the closure pointer might come from multiple places, operator() needs a method call, bind expressions need who knows what (probably method call)...

In practice, that whole dispatch can probably be compiled down to a simple virtual method call, but then having a special "null" object that just throws an expression doesn't cost anything, now does it?

Post

mystran wrote: Fri Mar 19, 2021 8:36 pm Let's see: Rust "solves" issues by forbidding the cases that are not trivial to begin with. Nim has a garbage collector. Zig promises to never implement any of the features that make large C++ projects manageable (seriously; I'm not exactly sure why any of these should be taken seriously).
Should be taken seriously because of the potential. I personally don’t need a C++ replacement doing the same old mistakes in a contemporary way. I know C++ well enough.

I guess that C doesn’t have any of those features that Zig lacks and there it is, having projects with a readability that C++ will never have.

The C++ projects that are manageable are C— via coding standard. A different subset on each shop and a lot of linting to prevent people fall on traps caused by language misdesign. I have seen many of those projects miserably fail though.

The bigger the project and the number of people involved, the better to have a readable language. For an expert working alone yes, C++ is a funny language with total control and funny puzzles to solve.

Notice that Nim can work without garbage collector, with reference counting and with soft real-time (audio plugin) non preemptive garbage collection. It is a compiled language.

https://nim-lang.org/docs/gc.html

Anyways, each one has its own preferences. Different strokes for different folks.

Post

rafa1981 wrote: Fri Mar 19, 2021 11:01 pm Anyways, each one has its own preferences. Different strokes for different folks.
I think you hit the nail there. My preference is very much C++ because that's the language where I find myself spending the least amount of time fighting the language and the most amount of time getting stuff done. C++ is not perfect, but on average I still find it much better than anything else.

The general attitude these days that everyone would want to use something else, anything else except C++ is just non-sense. C++ is a fine language, you just need to learn it properly and not hire college graduates.

Post

C++, is there any middle ground between composition and inheritance?

C# has the export of types, which is pretty cool IMO.

My major gripe with C/C++ is scope. Everything is top down, so you must know in advance, the methods in use, or else risk re-writing a lot of code. Some things like aliasing are not able to be resolved by the compiler, but aliasing is how to write generic code, which hopefully the compiler can explicitly resolve, with no UB.

Post

mystran wrote: Fri Mar 19, 2021 10:49 pm There is: cast to unsigned, perform operation, cast back to signed. Addition, subtraction and multiplication result in the same bit-pattern in both unsigned and 2s-complement signed arithmetic and division cannot overflow.
Actually you're right, I could have written this intrinsic myself, rather than "manually" writing all those casts.

Edit: OTOH I wonder, what are these specific "important optimizations" you were referring to.
In practice, that whole dispatch can probably be compiled down to a simple virtual method call, but then having a special "null" object that just throws an expression doesn't cost anything, now does it?
When I checked, it was complied to essentially a raw function pointer call, but there was no "null object stub", it was done by an explicit check. Maybe it's compiler dependent, though. But I don't care too much if it's compiler dependent, I'm having certain (hopefully not unreasonable) expectations about the generated code, and I expect it to be portable across a range of compilers. Anyway, in this specific case I actually didn't have to use std::function, used a raw function pointer instead, but then this is one more of these irregularities.

Post

As you I don’t see myself switching from C++ to Rust because the problems it solves don’t overweight the cost of me switching. Same for Nim and Zig. For different reasons.

But if we speak in 10 years from now, I ‘d prefer any of those two gain adoption better than Rust.

This is the part we fully agree. We are not riding hype trains.

We don’t agree in that it is a fine language. I consider it fugly and I’m reminded of it constantly. When training some colleagues it becomes evident.

You can’t run a project with college graduates, but they need a first job, and training them on C++ is a PITA.

Post

I am running with Rust, also because I don't have to care about memory management BS. Are my values in C++ stored in a vector, do I need to copy them when working with the vector, which smart pointer do I use. Are my pointers fine? Etc. All of these problems get resolved in rust.
Modern code is full of dependencies, no need to overload the overloaded brain with stuff a compiler should figure out.
Life is to short to worry about these low level problems.

The rust compiler has problems in lining stuff etc. Etc. But this may get fixed.
C++ is unfixable.

Yes I also tried other languages like Haxe, I wasn't satisfied.

Post

Q9000 wrote: Mon May 17, 2021 1:37 am Yes I also tried other languages like Haxe, I wasn't satisfied.
God and Buddha together could not move me to attempt DSP in Haxe. I’m amazed anyone at all has tried.

Rust is actually good. It’s pretty close to what I wanted years ago from a “unicorn” language, after I’d met Haskell and used it to write a whole bunch of stuff I had no business trying to write in Haskell. Back then there were rumors about a few other languages, like Clay and BitC, that never reached completion. The thing that impresses me most about Rust is how it has thrived where those other languages withered. The people behind it are brilliant, and more importantly, professional. It’s a shame Mozilla deemed it necessary to fire them all, but with the new foundation set up, and companies like Google and Amazon throwing their weight behind it, I think Rust has survived the single biggest threat to its existence for at least the next decade.
I hate signatures too.

Post

Q9000 wrote: Mon May 17, 2021 1:37 am I am running with Rust, also because I don't have to care about memory management BS. Are my values in C++ stored in a vector, do I need to copy them when working with the vector, which smart pointer do I use. Are my pointers fine? Etc. All of these problems get resolved in rust.
If you're writing DSP code then you should care about memory management exactly as much in Rust as you do in C++ since you can't do any of it in your DSP thread in either. Additional you generally do it more or less exactly the same way in both languages anyway (eg. the vectors work the same, Box<T> is basically std::unique_ptr with a different name and so on).

Safe Rust is also not workable for performance sensitive numerical code, because the overhead of runtime bounds checking on arrays is ridiculous and you really want to relax strict IEEE semantics so that your compiler can actually do some floating point optimization.

So how about writing the GUI side in Rust? Has somebody actually figured out how to write GUI code in Rust yet? Widget demos are all great, but a realistic GUI is a large pile of event-driven state and a complex set of cyclic dependencies and that's something that affine typing truly struggles with. It also happens to be the problem domain where classic OOP inheritance and encapsulation truly shine.

So like.. what exactly is Rust supposed to do for me?

TL;DR version: We use C++ because it gives us zero-cost abstractions and manyof the supposed advantages of Rust are nowhere near zero cost, even though the Rust commonly really loves to pretend otherwise.

ps. The worst part of Rust is the toxic community who's solution to every real problem is to insist that "it's not the Rust way" without ever providing any real solutions except "use unsafe" which sort of defeats the whole purpose.

Post

I was to write the same, if you don't care about memory management, and that includes memory layout/cache-friendliness, then your DSP code is going to be suboptimal.

On DSP code as far as I can see, you can get very far by just having fixed size arrays (e.g. for filters) or a one time allocated vector for the whole effect lifetime and then passing pointers/references around. Why smart pointers on DSP code?

Post Reply

Return to “DSP and Plugin Development”