Hacker News new | ask | show | jobs
by qalmakka 1790 days ago
C++ definitely hasn't a weaker type system than "newer" languages like Java - if any, it is much more richer and complex than most languages out there. What's happening here is a type conversion that has to be in place due to C not having a boolean type until 1999. C++ attempts to construct a boolean from the argument of an `if()`, and given that bool can be constructed from int, the conversion succeedes.

You can define your own conversion operators to boolean, too, which are very useful for stuff like smart pointers and similar classes that may or may not have a value.

  struct A {
     std::string value;
  
     explicit operator bool() const {
        return this->value.size();
     }
  };
  
  // ...
  
  A a {};
  if (!a) { 
     // ...
  }
1 comments

"Not as weak as Java" is not a very interesting benchmark, as Java also has a poor type system. C++'s type system is pitiable relative to those of Rust, Haskell, OCaml, and SML.

Moreover, in addition to being less expressive than them, C++'s type system is also weak, in formal sense, by allowing many implicit type conversions - which is one of the issues that I was complaining about. The fact that it "has to be in place due to C not having a boolean type until 1999" doesn't make it any less weak.

The fact implicit conversions exist doesn't really weaken the type system, the two are different concepts. If you define an overload or a type specialization on both the compiler will call the right version without any ambiguity. You can even define your own implicit conversions itself, and sometimes they have their uses, like when you need to wrap values in proxies but you still don't want the user to actually have to know that.

The template system and stuff like auto and such are crazy powerful. The Rust typesystem doesn't suffer from the C compatibility baggage and has been designed almost 30 years later, so it is amazing C++ can be so powerful and feel modern despite it being a forty years old language.

I write both Rust and C++ and I have to say, you can do a lot of type safe stuff in C++ too. Most patterns can be backported from Rust and while the ergonomics are not obviously at par, there is still a lot you can do. In C++you can do a crazy amount of metaintrospection at compile time that Rust can only do using procedural macros. Also constexpr and C++20's constinit and consteval are still more powerful than Rust's const.

It is nothing to be amazed, other than we are finally able to enjoy such type systems in mainstream languages.

PL/I and Algol 68 were equally powerfull, or if you research into was being done in Xerox PARC workstations.

If anything, we are 30 years too late for where we could have been if it wasn't for UNIX winning the worstation market.

The type system in C++ is pretty strong actually, it just has too many implicit conversions. It's not like the compiler doesn't know that this value is not a bool.
Which of "Rust, Haskell (core Haskell), OCaml, and SML" is able to parametrize types on values, à la template<auto> ?
It only supports integers so far, which c++ could do pretty much since before it was even standardized 25 years ago. Doesn't seem to support as wide of a type menagerie as C++20's NTTP. Does it even support parametrization over function pointers ?
> It only supports integers so far

Integer-like things. C++ char is similar to Rust's u8 or i8 but Rust's char and bool are very deliberately not just integers.

> Does it even support parametrization over function pointers ?

Can you give a clear example ? I think the answer is "No" but I struggled to put together an application for the feature I'm imagining.

You would like to define a type, which is parameterised not by the type of function, but by specific functions? So, for example I can make a foo<sum> and a foo<average> and those are distinct types which presumably internally are using the provided function to behave differently?

Except all the examples I think of come out better with just parametrisation over traits instead. So a clear example from you might illuminate whether this is a sizeable hole or just a difference of philosophy.

> Integer-like things. C++ char is similar to Rust's u8 or i8 but Rust's char and bool are very deliberately not just integers.

sure, but there's an easy mapping from char / bool to integers. C++ supports parametrizing on values of struct type, which is a clear increase in expressive power:

    struct foo {
      int count;
      float init;
    };
    
    template<auto arg> struct bar {
      float x[arg.count];
      
      bar() {
       for(int i = 0; i < arg.count; i++) 
         x[i] = arg.init;
      }
    };
    
    constexpr foo f{ .count = 123, .init = 4.56 };
    bar<f> b;


> You would like to define a type, which is parameterised not by the type of function, but by specific functions? So, for example I can make a foo<sum> and a foo<average> and those are distinct types which presumably internally are using the provided function to behave differently?

yes, it's pretty much the only way in C++ to have zero-cost callbacks / strategy pattern using function pointers since the compiler can directly inline the function pointer everywhere in the type's implementation instead of having to go and read it from some variable at run-time

In extremely limited form. That should change in the future, though.
Much more usefully, in C++ you can also parametrize over templates, which I think you still can't in rust.

You can't parametrize over namespaces (at least not directly) which is an annoying and arbitrary restriction.

> You can't parametrize over namespaces (at least not directly) which is an annoying and arbitrary restriction.

It's not a restriction, namespaces are neither types nor values so they would need specific support. Given that you can (ab)use classes with static members as namespaces which are also a type it's simply that nobody cared enough to add support for templating over real namespaces.

You are right it is not a restriction in the sense that is explicitly disallowed; what I meant is that it would take very little to add support for parametrizing over namespaces.

Stateless structures are a workaround (so is adl driven by a template parameter), still direct support would be nice.

For those languages, I don't really see a need to paremtrize types on values. Because of the AMAZING generics support.

But that may just be the blub paradox [1] in action

[1] https://wiki.c2.com/?BlubParadox

Well the fact you couldn't reasonably use arrays without Generic Const hit stable in Rust 1.51 says otherwise.

Defining templates on values is very useful, especially in C++ where you can provide template specializations. You can do a whole lot of metaprogramming and compile-time stuff that way.

I don't think weak has a formal sense in any formal sense.
Java has a poor type system, but not as poor as that of C++.
This is uttely false. I desume you haven't used C++ in the last 10 years, or at least you didn't delve deep enough into it to really understand how powerful (while bonkers) the C++ type system is.

C++ has a much, MUCH more stronger type system with true generics, value types, const-correctness, compile time reflection and dispatching, ...

In Java, everything is a reference, except when it's not (which is a design mistake that .NET fixed, IMHO). Some stuff in Java is plain "magic", like type erasure and boxing, while C++ might well be drowning in its own sea of utter madness but at least tries to be somewhat consistent (for instance, there are no "magic" types, when you do `int { 3U }` you are "constructing" an integer, when you do `bool x { 33 }` you applying the implicitly defined `bool(int)` constructor from bool. You can define your own conversions, and you can define your own custom types that behave and can be used like built-in ones (see smart pointers, iterators, ...).

Java _seems_ stronger typed because it generally doesn't allow integer promotions and implicit conversions, but these are concepts that are orthogonal to the type system, `bool` and `char` are different, distinct types and if you specialize a template for T<bool>, it won't apply to `char` unless a conversion happens, and if it does so, it is still operating on `bool`, not char - it is constructing a type from another, the fact this happens is simply hidden from you, like Java and boxing (which ironically is an implicit conversion).

The Java delegates pretty much everything to the JVM, and that's reflected in the language design. Java is a simple language that does not do a lot at compile time, relying on runtime facilities to mitigate these shortcomings. See for instance how everything can always decay to a reference to Object, implicitly, everywhere, requiring casts (i.e. runtime assertions) to restore type safety - that's basically a safer `void*`.

1995's Java was clearly too limited, I understand they wanted a fresh start from the ugliness of '90s C++, but they straight removed too much for the sake of simplicity. The current crop of languages, which largely rejected the Java model is kind of a symbol of what went wrong with Java, IMHO.

The fact certain features had been "hacked" on top of the language using what was already there (see generics, boxing, ...), often introducing features that act like "magic", and can't be overridden by the user is bad, and shows how limited the original language was. The same way you can't override operators, you can't define custom boxing rules for your types (mostly because you won't be able to define custom value types until Valhalla is released).

Modern C++ allows you to write safe and solid code using compile-time features and the type system. While stuff like <type_traits>, SFINAE, template metaprogramming and such are definitely not "nice", they are extremely powerful and if used correctly eliminate completely certain issues from ever happening. If you only use smart pointers, references, containers, moves and by-value semantics you won't get crashes from nulls, ever. You won't have memory leaks, and after lots of fighting with the compiler, there's a high chance your code will work straight away (unless you messed up the logic). This is not that far from Rust, as far as my experience goes - I still hope for Rust to mostly replace C++ in the end, but for now C++20 is a very solid substitute and a good choice (and feels much more modern and powerful than Java could or has ever been).

All versions of C++ inherit type system from C and keep it for backward compatibility. Indeed you can opt into a stronger dialect with right compiler flags, but this is used as widely as Prolog, because the rest of ecosystem uses an incompatible dialect and will fight against you. C++20 is a substitute of C++17, not of other languages.
The desire of maintaining compatibility with C at all cost is both the main selling point of C++ and the source of almost all of its madness. C was full of weird quirks (null terminated strings, enums that decayed to int, char* that could represent arbitrary data, weird syntax inherited from B, nonsensical arrays, ...) but its simplicity has always kinda kept everything down to a tameable level of chaos.

C++ took this quirky base and pushed it (abused it?) in ways that go way beyond what can be considered "sane"; see for instance the bajillion ways operator new() can be redefined, the fact there are at least 10 ways to initialize something, or the fact that features can be _discovered_ and not designed (like CRTP). Still, this wealth of features together with the unwavering (and arguably masochistic) Herculean efforts of the ISO C++ committee to modernize the language make it, by far, one of the most flexible languages around. Every single time I use C++ I discover a new amazing way to do something that had never crossed my mind; call me crazy, but I think this makes programming fun, a detail that is often (wrongly) neglected.

I just wish there would be a -fsafe compiler flag instead of having to deal with all the sanitizers and debug mode flags, but it is what it is.