My understanding is that any organizations have an absolute ban on using anything with AGPL because it affects any other code that touches it and its considered too high a risk.
Not exactly how it works but I do understand the concern.
The other option is to "just give it away" by not having it and being sherlocked... Now? You have to ask permission first, commercial licensing is a thing.
Yeah, these kinds of "orthogonal" things that you want to set up "on the outside" and then have affect the "inner" code (like allocators, "io" in this case, and maybe also presence/absence of GC, etc.) all seem to cry out for something like Lisp dynamic variables.
It depends on how you do it. XSLT 2.0 had <xsl:tunnel>, where you still had to declare them explicitly as function (well, template) parameters, just with a flag. No explicit control over levels, you just get the most recent one that someone has passed with <xsl:with-param tunnel="yes"> with the matching qualified name.
For something like Zig, it would make sense to go one step further and require them to be declared to be passed, i.e. no "tunneling" through interleaving non-Io functions. But it could still automatically match them e.g. by types, so that any argument of type Io, if marked with some keyword to indicate explicit propagation, would be automatically passed to a call that requires one.
I really don't like Object Oriented programming anywhere. Maybe Smalltalk had it right, but I've not messed with Pharo or anything else enough to get a feel for it.
CLOS seems pretty good, but then again I'm a bit inexperienced. Bring back Dylan!
(oh and I think you can write a whole book on the different ways to initialize variables in C++).
The result is you might be able to use C++ to write something new, and stick to a style that's readable... to you! But it might not make everyone else who "knows C++" instantly able to work on your code.
Overloaded operators are great. But overloaded operators that do something entirely different than their intended purpose is bad. So a + operator that does an add in your custom numeric data type is good. But using << for output is bad.
The first programming language that used overloaded operators I really got into was Scala, and I still love it. I love that instead of Java's x.add(y); I can overload + so that it calls .add when between two objects of type a. It of course has to be used responsibly, but it makes a lot of code really more readable.
> The first programming language that used overloaded operators I really got into was Scala, and I still love it. I love that instead of Java's x.add(y); I can overload + so that it calls .add when between two objects of type a. It of course has to be used responsibly, but it makes a lot of code really more readable.
The problem, for me, with overloaded operators in something like C++ is that it frequently feels like an afterthought.
Doing "overloaded operators" in Lisp (CLOS + MOP) has much better "vibes" to me than doing overloaded operators in C++ or Scala.
Tangential, but Lua is the most write-only language I have had pleasure working with. The implementation and language design are 12 out of 10, top class. But once you need to read someone else's code, and they use overloads liberally to implement MCP and OODB and stuff, all in one codebase, and you have no idea if "." will index table, launch Voyager, or dump core, because everything is dispatched at runtime, it's panic followed by ennui.
It works with arrays (both fixed size, and dynamically sized) and arrays; between arrays and elements; but not between two scalar types that don't overload opBinary!"~", so no it won't work between two `ushorts` to produce a `uint`
Those languages need a dedicated operator because they are loosely typed which would make it ambiguous like + in JavaScript.
But C++ doesn't have that problem. Sure, a separate operator would have been cleaner (but | is already used for bitwise or) but I have never seen any bug that resulted from it and have never felt it to be an issue when writing code myself.
Though then you can have code like "hello" + "world" that doesn't compile and "hello" + 10 that will do something completely different. In some situations you could actually end up with that by gradual modification of the original code..
Python managed to totally confuse this. "+" for built-in arrays is concatenation. "+" for NumPy arrays is elementwise addition. Some functions accept both types. That can end badly.
Regrettably, “intended purpose” is highly subjective.
Sure, << for stream output is pretty unintuitive and silly. But what about pipes for function chaining/composition (many languages overload thus), or overriding call to do e.g. HTML element wrapping, or overriding * for matrices multiplied by simple ints/vectors?
Reasonable minds can and do differ about where the line is in many of those cases. And because of that variability of interpretation, we get extremely hard to understand code. As much as I have seen value in overloading at times, I’m forced to agree that it should probably not exist entirely.
Let's say I have matrices, and I've overloaded * for multiplying a matrix by a matrix, and a matrix by a vector, and a matrix by a number. And now I write
a = b * c;
If I'm trying to understand this as one of a series of steps of linear algebra that I'm trying to make sure are right, that is far more comprehensible than
a = mat_mult(b,c);
because it uses math notation, and that's closer to the way linear algebra is written.
But if I take the exact same line and try to understand exactly which functions get called, because I'm worried about numerical stability or performance or something, then the first approach hides the details and the second one is easier to understand.
This is always the way it goes with abstraction. Abstraction hides the details, so we can think at a higher level. And that's good, when you're trying to think at the higher level. When you're not, then abstraction just hides what you're really trying to understand.
The thing is code without operator overloading is also hard to understand because you might have this math thing (BigIntegers, Matrices) and you can't use standard notation.
Why is using << for output bad? From what I've seen, it's usually not too hard to figure out that the left hand side is some kind of stream and not a number.
If you've done any university-level maths you should have seen the + sign used in many other contexts than adding numbers, why should that be a problem when programming?
There is usually another operator used for concatenation in math though: | or || or ⊕
The first two are already used for bitwise and logical or and the third isn't available in ASCII so I still think overloading + was a reasonable choice and doesn't cause any actual problems IME.
So, what programmers wanted (yes, already before C++ got this) was what are called "destructive move semantics".
These assignment semantics work how real life works. If I give you this Rubik's Cube now you have the Rubik's Cube and I do not have it any more. This unlocks important optimisations for non-trivial objects which have associated resources, if I can give you a Rubik's Cube then we don't need to clone mine, give you the clone and then destroy my original which is potentially much more work.
C++ 98 didn't have such semantics, and it had this property called RAII which means when a local variable leaves scope we destroy any values in that variable. So if I have a block of code which makes a local Rubik's Cube and then the block ends the Rubik's Cube is destroyed, I wrote no code to do that it just happens.
Thus for compatibility, C++ got this terrible "C++ move" where when I give you a Rubik's Cube, I also make a new hollow Rubik's Cube which exists just to say "I'm not really a Rubik's Cube, sorry, that's gone" and this way, when the local variable goes out of scope the destruction code says "Oh, it's not really a Rubik's Cube, no need to do more work".
For trivial objects, moving is not an improvement, the CPU can do less work if we just copy the object, and it may be easier to write code which doesn't act as though they were moved when in fact they were not - this is obviously true for say an integer, and hopefully you can see it will work out better for say an IPv6 address, but it's often better for even larger objects in some cases. Rust has a Copy marker trait to say "No, we don't need to move this type".
In particular, move is important if there is something like a unique_ptr. To make a copy, I have to make a deep copy of whatever the unique_ptr points to, which could be very expensive. To do a move, I just copy the bits of the unique_ptr, but now the original object can't be the one that owns what's pointed to.
Sure. Notice std::unique_ptr<T> is roughly equivalent to Rust's Option<Box<T>>
The C++ "move" is basically Rust's core::mem::take - we don't just move the T from inside our box, we have to also replace it, in this case with the default, None, and in C++ our std::unique_ptr now has no object inside it.
But while Rust can carefully move things which don't have a default, C++ has to have some "hollow" moved-from state because it doesn't have destructive move.
I personally think that operator overloading itself is justified, but the pervasive scope of operator overloading is bad. To me the best solution is from OCaml: all operators are regular functions (`a + b` is `(+) a b`) and default bindings can't be changed but you can import them locally, like `let (+) = my_add in ...`. OCaml also comes with a great convenience syntax where `MyOps.(a + b * c)` is `MyOps.(+) a (MyOps.(*) b c)` (assuming that MyOps defines both `(+)` and `(*)`), which scopes operator overloading in a clear and still convenient way.
A benefit of operator overloads is that you can design drop-in replacements for primitive types to which those operators apply but with stronger safety guarantees e.g. fully defining their behavior instead of leaving it up to the compiler.
This wasn't possible when they were added to the language and wasn't really transparent until C++17 or so but it has grown to be a useful safety feature.
However C++ offers several overloads where you don't get to provide a drop-in replacement.
Take the short-circuiting boolean operators || and &&. You can overload these in C++ but you shouldn't because the overloaded versions silently lose short-circuiting. Bjarne just didn't have a nice way to write that so, it's not provided.
So while the expression `foo(a) && bar(b)` won't execute function bar [when foo is "falsy"] if these functions just return an ordinary type which doesn't have the overloading, if they do enable overloading both functions are always executed then the results given to the overloading function.
Edited:: Numerous tweaks because apparently I can't boolean today.
A pet peeve of mine is when people claim C++ is a superset of C. It really isn't. There's a lot of little nuanced differences that can bite you.
Ignore the fact that having more keywords in C++ precludes the legality of some C code being C++. (`int class;`)
void * implicit casting in C just works, but in C++ it must be an explicit cast (which is kind of funny considering all the confusing implicit behavior in C++).
C++20 does have C11's designated initialization now, which helps in some cases, but that was a pain for a long time.
enums and conversion between integers is very strict in C++.
`char * message = "Hello"` is valid C but not C++ (since you cannot mutate the pointed to string, it must be `const` in C++)
C99 introduced variadic macros that didn't become standard C++ until 2011.
C doesn't allow for empty structs. You can do it in C++, but sizeof(EmptyStruct) is 1. And if C lets you get away with it in some compilers, I'll bet it's 0.
Anyway, all of these things and likely more can ruin your party if you think you're going to compile C code with a C++ compiler.
Also don't forget if you want code to be C callable in C++ you have to use `extern "C"` wrappers.
> It really isn't. There's a lot of little nuanced differences that can bite you.
These are mostly inconsequential when using code other people write. It is trivial to mix C and C++ object files, and where the differences (in headers) do matter, they can be ifdefed away.
> void * implicit casting in C just works, but in C++ it must be an explicit cast (which is kind of funny considering all the confusing implicit behavior in C++).
This makes sense because void* -> T* is a downcast. I find the C behavior worse.
> enums and conversion between integers is very strict in C++.
As it should, but unscoped enums are promoted to integers the same way they are in C
> `char * message = "Hello"` is valid C but not C++
Code smell anyway, you can and should use char[] in both languages
You didn't mention the difference in inline semantics which IMO has more impact than what you cited
Not for temporaries initialized from a string constant. That would create a new array on the stack which is rarely what you want.
And for globals this would preclude the the data backing your string from being shared with other instances of the same string (suffix) unless you use non-standard compiler options, which is again undesirable.
In modern C++ you probably want to convert to a string_view asap (ideally using the sv literal suffix) but that has problems with C interoperability.
Right, I've checked that char foo = "bar"; is indeed the same as the const char variant (both reference a string literal in rodata), which IMO makes it worse.
About string literals, the C23 standard states:
It is unspecified whether these arrays are distinct provided their elements have the appropriate values. If the program attempts to modify such an array, the behavior is undefined.
therefore `char foo = "bar";` is very bad practice (compared to using const char).
I assumed you wanted a mutable array of char initializable from a string literal, which is provided by std::string and char[] (depending on usecase).
> In modern C++ you probably want to convert to a string_view asap (ideally using the sv literal suffix)
And I think you're downplaying many of the ones I mentioned, but I think this level of "importance" is subjective to the task at hand and one's level of frustrations.
The two will also continue to diverge over time, after all, C2y should have the defer feature, which C++ will likely never add. Even if we used polyfills to let C++ compilers support it, the performance characteristics could be quite different; if we compare a polyfill (as suggested in either N3488 or N3434) to a defer feature, C++ would be in for a nasty shock as the "zero cost abstractions" language, compared to how GCC does the trivial re-ordering and inlining even at -O1, as quickly tested here: https://godbolt.org/z/qoh861Gch
I used the [[gnu::cleanup]] attribute macro (as in N3434) since it was simple and worked with the current default GCC on CE, but based on TS 25755 the implementation of defer and its optimisation should be almost trivial, and some compilers have already added it. Oh, and the polyfills don't support the braceless `defer free(p);` syntax for simple defer statements, so there goes the full compatibility story...
While there are existing areas where C diverged, as other features such as case ranges (N3370, and maybe N3601) are added that C++ does not have parity with, C++ will continue to drift further away from the "superset of C" claim some of the 'adherents' have clung to for so long. Of course, C has adopted features and syntax from C++ (C2y finally getting if-declarations via N3356 comes to mind), and some features are still likely to get C++ versions (labelled breaks come to mind, via N3355, and maybe N3474 or N3377, with C++ following via P3568), so the (in)compatibility story is simply going to continue getting more nuanced and complicated over time, and we should probably get this illusion of compatibility out of our collective culture sooner rather than later.
Not sure I understand, since they're available in c++ designated initializes are one of the features I use most, to the point of making custom structs to pass the arguments if a type cannot be changed to be an aggregate. It makes a huge positive difference in readability and has helped me solve many subtle bugs ; and not initializing things in order will throw a warning so you catch it immediately in your ide
The problem is that there are a lot of APIs (even system and system-ish ones) that don't want to specify the order of their fields (or outright differ between platforms). Or that can't use a meaningful order due to ABI compatibility, yet the caller wants to pass fields in a meaningful order.
platform APIs like this are likely much less less than 1% of the things I call in my codebases. The few files i have open right now have absolutely no such call.
> A pet peeve of mine is when people claim C++ is a superset of C. It really isn't. There's a lot of little nuanced differences that can bite you.
> Ignore the fact that having more keywords in C++ precludes the legality of some C code being C++. (`int class;`)
Your very first example reverses the definitions of superset and subset. "C++ is a superset of C" implies that C++ will have at least as many, if not more, keywords than C.
> void * implicit casting in C just works, but in C++ it must be an explicit cast
In C, casting a `void *` is a code smell, I feel.
Most confusing one is how the meaning of `const` differs between C and C++; I'm pretty certain the C `const` keyword is broken compared to `const` in C++.
Yes, casting in general is a code smell. (Not as in never use it, but as in use it as less as possible of course.)
The thing is, void was introduced exactly to represent this use-case without casting. The type to represent random amount of bytes, check which type and then cast is called char *. void * is for when you know exactly which type it has you just need to pass it through a generic interface, which isn't supposed to know or touch the type. The other use case is for when the object hasn't a type yet, like from malloc.
Previously everything was typed char *, then void * was used to separate the cases: casting from char *, don't cast from void *. Now in C++ both are the same again.
reply