← Back to context

Comment by jasode

14 hours ago

>Takeaway #1: "C and C++ are different: don’t mix them, and don’t mix them up"

Where "mixing C/C++" is helpful:

- I "mix C in with my C++" projects because "sqlite3.c" and ffmpeg source code is written C. C++ was designed to interoperate with C code. C++ code can seamlessly add #include "sqlite3.h" unchanged.

- For my own code, I take advantage of "C++ being _mostly_ a superset of C" such as using old-style C printf in C++ instead of newer C++ cout.

Where the "C is a totally different language from C++" perspective is helpful:

- knowing that compilers can compile code in "C" or "C++" mode which has ramifications for name mangling which leads to "LINK unresolved symbol" errors.

- knowing that C99 C23 has many exceptions to "C++ is a superset of C" : https://en.wikipedia.org/wiki/Compatibility_of_C_and_C%2B%2B...

The entire I/O streams (where std::cout comes from) feature is garbage, if this was an independent development there is no way that WG21 would have taken it, the reason it's in C++ 98 and thus still here today is that it's Bjarne's baby. The reason not to take it is that it's contradictory to the "Don't use operator overloading for unrelated operations" core idea. Bjarne will insist that "actually" these operators somehow always meant streaming I/O but his evidence is basically the same library feature he's trying to justify. No other language does this, and it's not because they can't it's because it was a bad idea when it was created, it was still a bad idea in 1998, the only difference today is that C++ has a replacement.

The modern fmt-inspired std::print and std::println etc. are much nicer, preserving all the type checking but losing terrible ideas like stored format state, and localisation by default. The biggest problem is that today C++ doesn't have a way to implement this for your own types easily, Barry illustrates a comfortable way this could work in C++ 26 via reflection which on that issue closes the gap with Rust's #[derive(Debug)].

  • Remember that C++ originally didn't have variadic templates, so something like std::format would have been impossible back in the day. Back in the day, std::iostream was a very neat solution for type safe string formatting. As you conceded, it also makes it very easy to integrate your own types. It was a big improvement over printf(). Historic perspective is everything.

  • > The biggest problem is that today C++ doesn't have a way to implement this for your own types easily

    I’m not sure about the stdlib version, but with fmtlib you can easily implement formatters for your own types. https://fmt.dev/11.0/api/#formatting-user-defined-types

    • I think the problem is that your idea of "easy" is "Here's a whole bunch of C++ you could write by hand for each type" while the comparison was very literally #[derive(Debug)]. I wasn't abbreviating or referring to something else, that's literally what Rust programmers type to indicate that their type should have the obvious boilerplate implementation for this feature, in most types you're deriving other traits already, so the extra work is literally typing out the word Debug.

  • > Don't use operator overloading for unrelated operations

    This disn't stop with <iostream>, they keep doing it - the latest example I can think of is std::ranges operations being "piped" with |.

  • Perfectly iostreams happy user since 1993.

    • int a;

      cin >> a;

      Then the program goes berserk as soon as the first non-number is read out of standard input. All the other "cin >> integer" lines are immediately skipped.

      Yes, I know about error checking, clearing error condition, discarding characters. But it's a whole lot of stuff you need to do after every single "cin>>" line. It makes the simplicity of cin not worth it.

      6 replies →

    • Same, as long as I stay the hell away from locales/facets.

      Type safe input/output stream types and memory backed streams served on a silver plate is a pretty decent improvement over C.

    • Then I suppose you don't care about:

      * Performance

      * Support for localization (as the format string and positions of values to format differ between languages).

      * Code reuse & dogfooding - the data structures used in iostreams are not used elsewhere, and vice-versa

      * C and OS interoperability - as you can't wrap a stream around a FILE* / file descritor

      * bunch of other stuff...

      iostreams work, but are rather crappy.

      7 replies →

  • Over the years, I have heard numerous complaints about C++ I/O streams. Is there a better open source replacement? Or do you recommend to use C functions for I/O?

  • >No other language does this, and it's not because they can't it's because it was a bad idea when it was created, it was still a bad idea in 1998, the only difference today is that C++ has a replacement.

    Hindsight is 20/20, remember that. Streams are not that bad of an idea and have been working fine for decades. You haven't named a problem with it other than the fact the operators are used for other stuff in other contexts. But operator overloading is a feature of C++ so most operators, even the comma operator, can be something other than what you expect.

    >The biggest problem is that today C++ doesn't have a way to implement this for your own types easily, Barry illustrates a comfortable way this could work in C++ 26 via reflection which on that issue closes the gap with Rust's #[derive(Debug)].

    You can trivially implement input and output for your own types with streams.

    You appear to be a Rust guy whose motive is to throw shade on C++ for things that are utterly banal and subjective issues.

    • What they mean is this:

           struct Foo {
             int a;
             float b;
             std::string c;
           };
      
      
           Foo foo;
           std::cout << foo;
      

      with no extra code. It's called reflection, where the compiler can generate good-enough code to generate a character-stream serialization of an object without any human intervention.

      1 reply →

C++ can seamlessly include C89 headers.

The C library headers for libraries I write often include C11/C99 stuff that is invalid in C++.

Even when they are in C89, they are often incorrect to include without the include being in an `extern "C"`.

  • Extern "C" around the prototypes is mandatory, otherwise your linker will search for C++ symbols, which cannot be found in the C libraries you pass it.

  • Clang supports C11 - 23 in C++, as well as some future C features like fixed-point integers. The main pain points with Clang are just the fundamental differences like void* and char, which don't typically matter much at an interoperability layer.

    • There's a lot of subtle differences between 'proper' C and the C subset of C++, since C++ uses C++ semantics everywhere, even for its C subset.

      Many C++ coders are oblivious to those differences (myself included before I switched from 'mainly C++' to 'mainly C') because they think that the C subset of C++ is compatible with 'proper' C, but any C code that compiles both in a C++ and C compiler is actually also a (heavily outdated) subset of the C language (so for a C coder it takes extra effort to write C++ compatible C code, and it's not great because it's a throwback to the mid-90s, C++ compatible C is potentially less safe and harder to maintain).

      For instance in C++ it's illegal to take the address of an 'adhoc-constructed' function argument, like:

          sum(&(bla_t){ .a = 1, .b = 2, .c = 3, .d = 4 });
      

      (godbolt: https://www.godbolt.org/z/r7r5rPc6K)

      Interestingly, Objective-C leaves its C subset alone, so it is always automatically compatible with the latest C features without requiring a new 'ObjC standard'.

      1 reply →

  • Yeah plenty of headers first have `#ifdef __cplusplus` and then they add `extern "C"`. And of course even then they have to avoid doing things unacceptable in C++ such as using "new" as the name of a variable.

    It takes a little bit of an effort to make a header work on C and C++. A lot less effort than making a single Python file work with Python 2 and 3.

    • The '#ifdef __cplusplus extern "C" { }' thing only removes C++ name mangling from exported symbols, it doesn't switch the C++ language into "C mode" (unfortunately).