Hacker News new | past | comments | ask | show | jobs | submit login
C++ 20: The Core Language (modernescpp.com)
282 points by juice_bus on Nov 5, 2019 | hide | past | favorite | 272 comments



I'm glad this hasn't turned into (so far) the usual "c++ is dumb" flame fest.

I've really enjoyed programming in c++17 for the last three years (I had the luxury of starting from an empty buffer) and find the language pretty expressive. If you are able to use it as a "new" language, ignoring its C roots and features that exist pretty much just for back compatibility, it's really quite expressive, powerful, and orthogonal.

I'm actually pretty glad the c++ committee has been willing to acknowledge and deprecate mistakes (e.g. auto_ptr), to squeeze out special cases (e.g. comma in subscripting) and to attempt to maintain generality. Conservatism on things like the graphics standard is reducing the chance of the auto_ptr or std::map mistakes.


> it's really quite expressive, powerful, and orthogonal

Expressive and powerful are definitely true, but I'm not so sure about 'orthogonal'.

Most C++ features seem to depend heavily on each other, and library decisions can impose restrictions/boilerplate on application code in many areas.

Especially as the language has evolved, features like exceptions, destructors, copy and move constructors, and operator overloading have become more and more indispensable. Template functions are still required for any kind of variance (e.g. you still can't have a non-template function that works on a const std::shared_ptr<const *base_type> or any derived type).

None of these is unbearable, but I do believe that even the modern subset of C++ is still the least orthogonal mainstream language in use (and definitely the most complicated by far). To be fair, it's also likely the most expressive mainstream language too, and still the most performant.


> you still can't have a non-template function that works on a const std::shared_ptr<const *base_type> or any derived type

I guess you meant, you can't have a function that accepts a shared_ptr to the base type without indirectly using template machinery?

Because you can most definitely have a function that does this that is not a function template.

    #include <memory>
    #include <iostream>
    
    
    struct foo {
     virtual ~foo() = default;
     virtual void bark() const { std::cout << "fork\n"; }
    };
    
    
    struct bar : foo {
     void bark() const override { std::cout << "bark\n"; }
    };
    
    
    void poke_so_it_barks(std::shared_ptr<foo const> const thing) {
     thing->bark();
    }
    
    
    int main() {
     poke_so_it_barks(std::make_shared<foo>());
     poke_so_it_barks(std::make_shared<bar>());
    }


Oops, I picked a bad example it seems... Apparently `shared_ptr` does some magic to simulate covariance through type conversion. It still breaks in other cases (a function which returns a `shared_ptr<derived>` can't override a function which returns a `shared_ptr<base>`, unlike with derived* and base* ), but it still probably covers most real-world cases.

If I had stuck with std::vector my point would have been better. Even though theoretically a `const std::vector<derived* >` IS-A `const std::vector<base* >`, that is, a read-only view of a collection of derived elements behaves the same as a read-only view of a collection of base elements, you can't pass the first to a function expecting the second in C++, unless you make your own function a template.


In that case, you have a point. The closest you can get to covariance and contravariance is simulating it with implicit conversion operators/constructors (which then often need to be templates).

But I don't see how this could be implemented without completely changing large parts of the language.

In the one case of covariance that actually exists, overriding `virtual base* factory()` with `derived* factory() override`, the overridden function knows that the function it overrides returns a `base* `, so it can just return a `base* `, too, and callers through the subclass pointer know that they can adjust the return value back to a `derived* `.

But even the closest cousin, argument contravariance (i.e. overriding `virtual foo(derived* )` with `foo(base* ) override`) doesn't seem feasible. Callers through the base pointer will pass a `derived* `, so the overridden function has to expect to be passed a `derived* `, and there is no adjustment that can be made to a `base* ` to turn it into a valid `derived* ` statically.

The vector example is even more impossible. Since the function expects all members of the `std::vector<base* > const` argument to be `base* `, a caller with a `std::vector<derived* >` would have to allocate a new vector and adjust each individual pointer, which would be a terrible hidden runtime cost. (If they had added a constructor to accept a range to complement the one that takes an iterator pair, that's exactly what would have happened, though.)


> Since the function expects all members of the `std::vector<base* > const` argument to be `base* `, a caller with a `std::vector<derived* >` would have to allocate a new vector and adjust each individual pointer, which would be a terrible hidden runtime cost.

I don't think I understand your point. A function which expects a `base* ` can already be passed a `derived* ` without any problems. Doing the same for a collection of `base* ` to a collection of `derived* ` is not fundamentally different, but it does come with significant complications. For one, the language would have to be sure that the collection is read-only, since a function which wants to add something to the collection can't be called with a collection of a different type.

More significantly, the language itself has no idea that `std::vector<T>` is a collection of Ts, while `std::function<T>` is not a collection. In principle, it may be possible that it could look at all of the members and decide each template argument's variance. Even if that is theoretically possible (I'm not sure it is), it is certainly far too complicated to be a useful feature.

That leaves us with the option of some kind of annotation for variance in template parameters (or at use-time, as in Java).


> A function which expects a `base* ` can already be passed a `derived* ` without any problems. Doing the same for a collection of `base* ` to a collection of `derived* ` is not fundamentally different

Since C++ has multiple inheritance, derived* differs from base* not just by reinterpretation, the pointer value could be different.

A single pointer can trivially be adjusted, but a heap-allocated, potentially huge array of pointers would require an equally huge heap allocation to store the adjusted pointers.


Got it, thanks for bearing with me! I wasn't aware of this complexity of multiple inheritance (I guess this is another example of how orthogonal the language is :) ).


How could the type system reason about when covariance is valid?


It could potentially use const-ness for these use cases, though I'm not sure that would be tractable.

More realistically, it could allow the programmer to express it explicitly, as in C# (C<in T, out V> is covariant in T and contravariant in V) or Java (C<? extends T,? super V> is covariant in the first parameter, contravariant in the second). This would not solve the problem that the co-/contra-variance of some types may depend on their const-ness, but it would allow at least a covariant, always read-only, std::vector_view<T> (similar to IEnumerable<in T> in C#, which can also be implemented by the invariant List<T>/std::vector<T>).


The thing that has really kept me from getting behind updates to the C++ universe is the lack of progress on improving the state of build tooling. It is miserably underengineered for modern, dependency-heavy environments. C++20 does introduce modules, which is a good push towards correcting the problem, but I'm still going to be "wait and see" on whether the actual implementation pans out.


Well, there's Conan, which helps a bit, but these days what I simply do is use CMake to download a package from GitHub or wherever and build it.

Sadly the C++ ABIs are standardized the way that C ABIs are (I'm OK with why but it's unfortunate in that it creates a barrier) so you have to have separate libraries compiled with g++ and clang++ if you use both on your platform (we use both because they catch different bugs, and for that matter exhibit different bugs). But it means you can't simply install, say, fmt in any system-wide directory like /usr/lib or /usr/local/lib

Just as an amusing side note: Common Lisp used to be criticized for the massive size of its libraries and later likewise C++. It was true they were quite large. Now both are criticized for their tiny libraries. Which by today's standards they are.


you can definitely use c++ libraries compiled with clang++ when your code is compiled with g++ and vice versa. It only gets funky when one uses libc++ and one libstdc++


...unless those libraries use standard library datastructures, as you point out.


No, GCC and clang are fully ABI compatible (modulo bugs of course). Libstc++ and libc++ are not, so use whatever is the standard library of your platform (i.e the default for calng and GCC) and things work fine.


Oh I see; we were speaking past each other.

Yes, gcc and clang use the same platform API so if they use the same headers (libstc++ or libc++) then they will indeed use identical structure layout etc.

I meant a "gcc-native" toolchain (gcc + libstdc++) vs "llvm native" (clang++ + libc++) having different layout (and there is even some interoperability between them thanks to work by the llvm team). I realize my need to do this (to try to minimize opting bugs) is a special case, and probably unusual.


There is not really anything more “native” about using libc++ with clang as opposed to libstdc++ other than the fact that they happened to be developed by the same project. Using clang with libstdc++ is extremely mainstream and normal.

Actually I would bet that even among clang users, libstdc++ is used more commonly on GNU/Linux (IDK for sure, but it’s my hunch).


parent says they wants to use libc++ to catch more bugs. Which is a reasonable use case.


> Just as an amusing side note: Common Lisp used to be criticized for the massive size of its libraries and later likewise C++.

Part of "size of libraries" is "mental size of libraries".

And C++ and Lisp and have very large mental spaces for their main core libraries. A "String", for example, carries a huge amount of mental baggage in those languages. In most other languages, a string is extremely straightforward because it was designed into the language from the start.


It's possible I'm just used to it, but I've never found std::string more complicated than, say, python (what's up with unicode in 2 vs 3?) or JavaScript (UTF-16 = surrogate pair pain).

It's essentially a std::vector<char> with a few random convenience features bolted on.

I guess some of the confusing points are: not unicode aware, string literals aren't std::strings by default, c_str returns a pointer to a buffer with length one greater than the string length, and the usual C++ quirks like why is there both data and c_str?


> the usual C++ quirks like why is there both data and c_str

The usual C++ response: for backwards compatibility, because data was not required to null-terminate prior to C++11.


>In most other languages, a string is extremely straightforward because it was designed into the language from the start.

I think one of the classic advantages of C++ over C is that you have the option of std::string instead of char arrays.

I don't program in C++ so I don't really know but I do a lot of pure C and the strings truly are a mess (despite being VERY straightforward)


One of the things I like about C++ is that there is the std::string for common uses, but then you can design your own string classes with defined conversions to std::string. Qt adds QString with lots of nice utility methods, UnrealEngine adds optimized string types, etc. So you can have custom tailored classes for the task at hand, but easy to convert around to the different types with std::string conversions defined.


One of the things I dislike about C++ is that any large project will have lots of code converting between a dozen custom and gratuitously different string types.


This is the #1 thing I love about C++ compared with Rust — I don’t want it to be easy to depend on thousands of things. I would rather use a small, curated, relatively standard set of libraries provided by my OS vendor or trusted third parties.

“Modern, dependency-heavy environments” are a symptom of the fact that certain ecosystems make it easy to get addicted to dependencies; I don’t think they’re a goal to strive towards.


That's throwing the baby out with the bathwater. Building a C++ project with even a few critical dependencies (e.g. graphics libraries, font libraries, a client for a database or other complex network protocol) is a massive hassle. Sometimes those are curated well, sometimes they're not--but they remain essential for many projects. By taking a hard line against the hypothetical explosion of low-quality dependencies, you compromise the ability to use even the "small, curated, relatively standard set" of dependencies that are genuinely essential.


It's not about the ease of use (you need just a couple of lines in CMake to trigger the pkg-config scripts that every lib in the distribution has). It's about the people who work hard on maintaining the distributions. That's where the quality comes from.

And not only for the libraries:

http://kmkeen.com/maintainers-matter/


I'm not entirely convinced that this is a bad thing. The dependency-heavy environments a la Node.js gave us some interesting security nightmares.


Yes Node is a nightmare, however, you don't need to use public repositories, that much is a choice.

Similarly, Cargo in Rust is an absolute dream to work with, you just run `cargo new <project>` and then `cargo build`. That's it. If you want to add a dependency you can do so from the public crates.io repository or a sub-path or a git repository.

No language should be without this core feature, so it'd be great to see C++ get it too.


Package managers of the OS distributions just work, so many people don't see it as a "core feature" for the language.


That's not the same thing at all. Different versions, sources, etc. Should they really be he job of the OS package manager even when statically linked?


When statically linked it's even simpler: the dependency is used only during the build.


Maybe I wasn't clear, I'm advocating a solid build system and build dependency manager should be part of the core.


What does this feature have to do with programming languages? Why do I need one dependency tracking tool for Rust, a separate one for Go, a separate one for JavaScript, etc?

I already have a dependency management tool for C++; it is called the Ubuntu package repositories. I don’t need another one baked into the language.


Ubuntu and other OS package managers are years out of date and they are at the wrong layer for serious projects because they are OS layer so not good for hermetic builds on build servers.

https://repl.it/site/blog/packager


> No language should be without this core feature, so it'd be great to see C++ get it too.

BTW modules won't address this. I'm not sure you did but some other down thread comments implied that some people thought it would.


OS distributions have package managers and package repositories that have maintainers who are mostly decoupled from the developers. So that takes care of the quality/security problems that arise in ecosystems like Node.js.

There is also C. The tooling and the "package manager for C++" would be expected to seamlessly work for C and be expected to be accepted and used by the C community.

(personally I use CMake + OS package manager)


Although I agree with your point, cmake+vcpkg goes a long way for hobby projects, cmake with a bit of custom scripting goes a long way for larger scale projects.

The cmake language might not be beautiful, but it does allow for simple sub-projects/library scripts once the main cmake script is set up properly.


I think the general thinking is that in the c/c++ world dependency management is the role of the build tool

This is currently really easy to do cleanly though CMake by using Hunter and toolchain files (don't use submodules or addexternalproject)

External tools like Conan are also unnecessary bc they introduce redundancy and extra maintenance


> build tooling...is miserably underengineered for modern, dependency-heavy environments.

My advice about build tooling is to use buck (from Facebook) or bazel (from Google). If you have an ex-Googler nearby, use Bazel. If you have an ex-Facebooker nearby, use buck. Otherwise flip a coin.


While I love these projects, neither one of these actually fix the problem of an ecosystem of dependencies.

Buck and Bazel were created for complex internal dependencies.

You'll have just as easy/hard of a time getting Boost installed with Bazel as you would for Make.

Or publishing an accessible library that depends on Boost.


> You'll have just as easy/hard of a time getting Boost installed with Bazel as you would for Make.

> Or publishing an accessible library that depends on Boost.

This is patently false. Here is the bazel dependency for boost (put this in your workspace file):

    git_repository(
        name = "com_github_nelhage_rules_boost",
        commit = "6d6fd834281cb8f8e758dd9ad76df86304bf1869",
        shallow_since = "1543903644 -0800",
        remote = "https://github.com/nelhage/rules_boost",
    )

    load("@com_github_nelhage_rules_boost//:boost/boost.bzl", "boost_deps")
    boost_deps()
No other installation necessary. If someone has a C++ compiler and Bazel installed that will allow you to build against Boost deps (e.g. "@boost//:callable_traits"). No downloading boost by hand, no building boost by hand, no managing it's build, bazel does all of that.

Bazel does have a dependency ecosystem, it's based off of hashed versions of publicly available git repos and http archives (and anything else you want really). Which means any code anywhere can be a dependency while still being reproducible. Additionally you can provide local build instructions for the external code, so you don't even need to rely on their build. The better way is to find someone who maintains a BUILD file for a repo (or mirror and maintain it yourself) but still.


Boost may have been a bad example, since it is a common dependency, without any sub-dependencies.

To beetwenty's point, the C++ ecosystem in general lacks the the level of support for dependency ecosystems that you find in Maven, pip, gem, npm, etc.

Bazel external dependencies are in fact a real pain point. See the 3+ year old Bazel Recursive WORKSPACE proposal. [1]

I was at the first Bazel Conf in 2017, and the external dependencies meeting was quite the row.

[1] https://bazel.build/designs/2016/09/19/recursive-ws-parsing....


I feel like that's a misunderstanding of the issue. Recursive dependencies are orthogonal to the issue of having an ecosystem of dependencies. There is a pattern for recursive dependency management (did you notice the `boost_deps` recursive dependency call?) that is currently fine for production that recursive workspaces will make better.

Also as demonstrated Boost is way easier to include in bazel than make which was the original issue under discussion.

I make a library that depends on boost, here is how you use it:

    git_repository(
        name="foo"
        ...
    )

    load("@foo//:foo/foo.bzl", "foo_deps")
    foo_deps()
magic


The trick is that now one will depend on somebody who is not boost for these nice mappings. I brought such a dependency for gRPC once and now it doesn't work with bazel 1.0. I either have to find the author, understand all the bazel trickery, or switch to something else, because most of these bindings depend on many bazel features.

So, bringing such multiple third party bandaid is currently not such a great idea. It would be a bit better if boost itself provided it.


Wasnt this because the dependency for gRPC was moved into bazel itself?

How is this different than most other build systems? You always have to rely on others.


What kinds of problems do you run into with Boost on Bazel? Are there no available build files you can just drop and use directly?


Ah. Yes, I wasn't the one who had to get Boost into Buck, so I don't know the pain. But I also have been told to avoid Boost for modern C++, so...


I need boost, but mainly because I use other packages than depend on boost.

I also use boost when the compiler is behind (e.g. boost::variant until apple's compiler caught up).

I can't wait to have a boost-free tree. but I suspect it will be a while.


Why would someone avoid Boost?


I have seen a library with a copy of boost in the include folder. Client code is forced to use this outdated version and must avoid transitive dependencies to boost. Please don't do that.


It’s a very heavy dependency, in many ways.


Look into Bazel, so far it has been a dream.


What's the specific problem? Big companies run 100K+ .so/.o systems.


What does orthogonal mean as an adjective for a programming language? I've never heard that word outside of math and I always get excited by a word that might expand my ability to think about code.


Every feature controls one "dimension" of a language, and can be freely combined with other features that control other "dimensions" without worrying about how they interact.

I am quoting the word "dimension", because applying a precisely defined mathematics concept to something messier (like programming) will require a certain amount of hand waving and squinting.


I first came across the notion of orthogonality for programming languages in Algol 68.

Perhaps it's easiest to give examples of what it's not. At first in C you could only assign scalars; to assign a struct or union you had to call memcpy() or something like it. Eventually you could assign aggregates, but you still can't assign arrays unless you hide them in structs. It took a while before you could pass or return an aggregate.

Exceptions of that sort are non-orthogonalities.


One place where this shows in a fairly technical way is in discussing instruction sets: https://en.wikipedia.org/wiki/Orthogonal_instruction_set

"In computer engineering, an orthogonal instruction set is an instruction set architecture where all instruction types can use all addressing modes. It is "orthogonal" in the sense that the instruction type and the addressing mode vary independently. An orthogonal instruction set does not impose a limitation that requires a certain instruction to use a specific register[1] so there is little overlapping of instruction functionality.[2]"

Its use here is more general, but I hope the specific example helps.


Honestly I think it's a silly word to use for features. In math orthogonal means roughly "at right angles" or "dot product equals zero". When two things are orthogonal it means that they are unrelated so you can deal with them independently which makes things simpler.

In describing features orthogonal is basically jargon for unrelated and adds no additional clarity imo.


You don't want to write unrelated code, you want to write orthogonal code.


Orthogonal means that aspect are independent of other aspects.

I.e. you can understand one aspect of a programming language (memory allocation, generic programming, standard library, mutability, control flow, pattern matching, etc.) without understand every aspect.

As a counterexample, if a language has generics, but only generics for a standard array type, that lacks orthogonality.


Imagine a programming language that gives you three different ways to add 2 numbers:

1 + 2;

std::add(1, 2);

std::int(1).add(std::int(2));

That's is not orthogonal at all, since we have 3 different ways to do the same thing for no discernible reason.

An orthogonal interface is one where there are fewer reasonable ways to do something, and each function/operation has it's role clearly defined, it doesn't try to do things unrelated to its main role, and the interface doesn't mix different layers of abstraction (like intertwining OOP and arithmetic in my 3rd example, when there already are native arithmetic operations).


I recommend the book: The pragmatic programmer. There's a section there explaining orthogonality by explaining a system that is not: a helicopter.

That example sticks with me.


Not the OP but guessing that in this context probably means independent of historical baggage.


last time I used C++ was in the mid-2000s. With all the new features since then, I assume the "proper" way/style to right C++ has changed since then?


Largest changes (most from C++11, some from C++14 and C++17) are:

- std::unique_ptr and std::shared_ptr (your code should not have any new/delete). It's easy to write code with clearly defined ownership semantics, and I've yet to create a memory leak.

- Proper memory model so that multithreading has defined semantics.

- Lots of syntactic sugar (auto, range-for, lambdas, structured bindings, if constexpr, etc.).

- More metaprogramming tools/fun.

C++11 alone is a very different world compared to what you are probably used to.


> - std::unique_ptr and std::shared_ptr (your code should not have any new/delete). It's easy to write code with clearly defined ownership semantics, and I've yet to create a memory leak.

I cannot begin to describe how huge a thing this was for me. I came to C++-17 (just about) from C++-98 -- a language I hated so much, I vowed I would never, ever write anything in it on purpose.

It is a massively different beast today from what it was, but the biggest thing of all is this change in how you deal with memory. It was like going from a particularly annoying and verbose aberation of C to a particularly fast and expressive version of Python.

YMMV, of course. In fact, it may vary to the point of becoming absolutely unrecognizable if you're dealing with a massive legacy codebase. But I have to say, I've become a very big fan in the last few years.


It's strange to even call them the same language, it's even stranger when you consider the same compiler will deal with both subsets of the language. It's at the point where someone tells you something is in C++ and you can't even be sure what that means until you look at their code.


I keep my head down in language war discussions, but when people start the usual round of complaints and ridicule of C++, this is exactly what I think: I'm not sure we're actually talking about the same language.


I believe even with C++98 you could have used boost::shared_ptr and boost::scoped_ptr. The addition to the STL effectively didn't change that much. It's just that when you used C++ back then, they probably weren't considered best practice yet.

More important in my opinion are changes to the language, like lambdas or auto.


Why weren't you using smart pointers before c++11? boost had them, and rolling your own is very easy.

I did the latter for a project that started around 2000 and never looked back.


I think the answer to that is that I wasn't an experienced enough developer at the time to know how to do that. ;)


It's still terribly verbose. C++17 barely started to get templates under control. Look at how many characters you need to say "if" and "true"


Not sure how to understand your concern. Yes, it's not 05AB1E. Yes, namespaces are necessary if you have code bases with millions of LOC. But for the most part, C++ is as verbose as you make your type and variable names.

Do you actually find it verbose to read or also verbose to type? I don't consider the latter an issue since 1. you normally spend much more time thinking than typing while programming, and 2. code is read much more often than it is written.


Generally good things, but I get triggered when I hear people call metaprogramming "fun." Such is the beginning of all sorts of woes.


Many of the improvements are to make metaprogramming increasingly unnecessary. I practically don't use it at all anymore.

But coding on C++ has become quite fun. Generic lambdas deserve much of the credit.


Some more:

* Hash maps in the standard library (every so often I have to go and look it up to remind myself that no, I’m not imagining things, C++03 really didn’t have hash maps in the standard library, as ludicrous as that sounds)

* Regexes

* Option and Variant types

* Move semantics

And my personal favorite...

* foo<bar<quux>> is no longer a syntax error


C++ doesn't like making arbitrary choices in standard libraries, and there are lots of performance tradeoffs and API shape choices to make in a language like C++ that refuses to make programers pay for anything they don't want.


I don't know if you mean formatting, in which case I don't believe anything has changed (pretty much anything goes) but yes, you can now do for (auto [x, y] : list_of_coordinates()) ... and things like that.


* is now deprecated.


This is not even remotely true. Let's say you have:

  std::unique_ptr<foo> m_foo;
How do you pass that into a function that can receive a pointer to foo, but does not require it? The only way to do so is to declare the function to take a foo* and pass it m_foo.get()

In summary, * will never be deprecated until C++ has support for something along the lines of Rust's borrowing and lifetimes.


I can't edit my original comment anymore, so I'll just write a clarification in the reply.

Many people have pointed out that C++17 has std::optional. While that's true, I don't think it'll deprecate raw pointers. I'll try to explain why.

Here's an example you can paste, compile and run:

  #include <iostream>
  #include <memory>

  struct foo
  {
    int thingamajig;
  };
  
  void borrow_optional_foo(foo * borrowed)
  {
    if (borrowed != nullptr)
    {
      borrowed->thingamajig = 42;
    }
  }

  int main()
  {
    std::unique_ptr<foo> owned = std::make_unique<foo>();
    owned->thingamajig = 17;
    std::cout << owned->thingamajig << std::endl;
    borrow_optional_foo(owned.get());
    std::cout << owned->thingamajig << std::endl;
    return 0;
  }
If you run it, it should print:

  17
  42
What would it look like if we wanted to use std::optional and get the same behavior?

  #include <iostream>
  #include <functional>
  #include <memory>
  #include <optional>

  struct foo
  {
    int thingamajig;
  };
  
  void borrow_optional_foo(std::optional<std::reference_wrapper<foo>> borrowed)
  {
    if (borrowed)
    {
      borrowed->get().thingamajig = 42;
    }
  }

  int main()
  {
    std::unique_ptr<foo> owned = std::make_unique<foo>();
    owned->thingamajig = 17;
    std::cout << owned->thingamajig << std::endl;
    borrow_optional_foo(std::make_optional(std::ref(*(owned.get()))));
    std::cout << owned->thingamajig << std::endl;
    return 0;
  }
As you can see, there's a tradeoff involved. On the one hand, you get crystal clear, descriptive type: std::optional<std::reference_wrapper<foo>> is clearly an optional reference to foo.

On the other hand, using it is absolutely atrocious: you have to write borrowed->get().thingamajig as opposed to borrowed->thingamajig, and std::make_optional(std::ref(*(owned.get()))) as opposed to owned.get()

Does it work? Absolutely. Is it crystal clear? Indisputably so. Will it deprecate raw pointers? I really, really doubt it, but that's just my opinion.


This is a great reply and I think it echoes what I've seen written elsewhere. For example, this one by Herb Sutter:

https://herbsutter.com/2013/06/05/gotw-91-solution-smart-poi...


> How do you pass that into a function that can receive a pointer to foo, but does not require it?

Same as you would have a function that can receive an `int` but does not require it. Or a receive a `std::vector` but does not require it.

I surmise that you mean "how do you pass any value (pointer or otherwise) into a function that can receive a value but does require it". And I surmise you have before used pointer indirection to pass that value because it has a conventional sentinel value of 0/NULL/nullptr.

You are correct in that optional values did not have a standardized solution, until C++17 std::optional.

If you don't have C++17, I recommend one of the equivalent third-party implementations:

* https://www.boost.org/doc/libs/1_52_0/libs/optional/doc/html...

* https://github.com/akrzemi1/Optional


std::optional does not support references (std::optional<MyVeryLargeType&>) however unlike boost, so this precludes usage with non-copyable types.


That was decided against for now, though I'm not sure why. [1]

In any case, you can use std::optional<std::reference_wrapper<MyVeryLargeType>>.

[1] https://stackoverflow.com/a/26895581


> In any case, you can use std::optional<std::reference_wrapper<MyVeryLargeType>>.

I would frankly give a negative code review to anyone who would do that.

You go from e.g.

    void my_function(T* foo, T* bar, int baz)
    {
      // ...
      foo->stuff(bar, baz);
    }
to

    void my_function(std::optional<std::reference_wrapper<T>> foo, std::optional<std::reference_wrapper<T>> bar, int baz)
    {
      // ...
      foo->get().call(&bar->get(), z);
    }
this also has more overhead (sizeof(std::optional<std::reference_wrapper<T>>) is twice the size of T* ), and let's not even start talking about calling conventions and the compile time cost of having to include both <functional> and <optional> everywhere.


It's hard to tell, but if T* foo can be NULL (and I assume that it can, though there's no visible indication), your first function is gonna segfault.

It's easy to tell that in the second function.

Second function makes for a better code review.


heh, to say that I originally wrote if(foo) and then replaced it by //... because that was not the point. Also, dereferencing an unset optional is also UB, so it would segfault all the same given the same preconditions.


Yes. The code is equivalent, but it's clear that the second is meant to optional.

The first has no such indication, except hopefully some human-readable comment that the pointer may be NULL.


> The first has no such indication, except hopefully some human-readable comment that the pointer may be NULL.

I don't understand. If the parameter was not meant to be sometimes null it would be a reference; not a pointer. The fact that a pointer is used is the "this may be null" indication.


I am not sure I understand what you mean, but now (c++17) you can use an optional in this case.


This is precisely what std::optional is for.


Not formally, but as you were responding to a comment on style: indeed, you don't really need * that often; there are more powerful, safer options in most cases.

But C++ is a systems programming language and * is still quite useful.


  > I had the luxury of starting from an empty buffer
Let's assume that I'm willing to flush the cache. Are there and C++17 learning resources that you might recommend? My day-to-day languages are Python and scripting languages.


Cppreference (learn by doing and look stuff up as you go)

https://cppreference.com

The ISO C++ Standard draft (unironically—it's actually quite readable and is actually up-to-date unlike a lot of books)

https://wg21.link/std

Tour of C++ (book) by Bjarne Stroustrup (start here, you can read it in one weekend)


Not sure I'd recommend the C++ standard for learning the language for a couple of reasons:

* It's gosh darn huge

* While not the hardest standard to read, it is still a standard, so has to be focused on really really low level detains and precise complete explanations instead of being a good learning resource

That said it is a great resource if you're ever confused about some corner of the language -- a lot of articles explain things slightly incorrectly.


For me, pls see my comment https://news.ycombinator.com/item?id=21456353

Also the books by the person whose blog spawned this comment thread seem to be pretty good.


I have seen comments that suggest you can write cross-platform applications (or at least shared business logic) in c++.

Is that the case? If so, how would you recommend someone go about learning c++?


As in: recompile and works? For sure, see for example a lot of the HEP/NP software which runs typically on Linux, BSDs (incl. MacOS) and I think even Windows.

As in: have the same binary? You could probably use the same object files, but shared libraries and executables probably won't work (without trickery), just because the file format is different.


I read Stroustrup's "tour of C++" then looked specifically for blogs that talked about C++ 17. And wrote a lot of code with all warnings enabled.

Yes you can write cross platform applications though perhaps only the engine, not UI.


Qt is great for cross platform UI, for Mac/Win/Linux at least


Last time I checked QT depended on a signal/slot mechanism that was not C++ compatible. There was a code generator.

Has this changed?


If you use CMake you don't see that generator. It automagically works.


No. But there are alternatives to moc like: https://github.com/woboq/verdigris .


Thank you. Very interesting work.


> Last time I checked QT depended on a signal/slot mechanism that was not C++ compatible.

what do you mean by "not C++ compatible" ? Qt is 100% C++ code. The few macros you see (signals:, slots:, emit) are #defined to nothing or public: / private: in a Qt header


In commercial settings, I encounter several barriers to using C++ versions newer than 2011:

(1) Most C++ code I encounter is C++11, and I deal with lots of projects. It's rarely sensible to change the language version of a large code base without a very good reason.

(2) Many developers are well-familiar with C++11.

(3) There's no widespread perception that C++14 or later bring compelling language improvements.

(4) Some (most?) C++11 developers are already uneasy with the complexity of C++11, and aren't eager to incur the learning curve and (perhaps) new set of pitfalls associated with newer C++ versions.

(5) Some C++11 developers look at the language trajectory, especially in terms of complexity, and have decided that their next language(s) will be e.g. Rust or Julia instead of C++14, C++17, etc.

I suspect these factors all contribute to the momentum that C++11 seems to have.


I don't encounter these barriers in commercial settings. The biggest barrier is compiler support if you target environments with different compiler toolchains. Each new version of C++ brings features and bug fixes that make code simpler. It actually reduces complexity from the perspective of the developer. The forcing function for upgrading C++ versions in my teams is always that it would greatly simplify some immediate problem or allow us to remove a gross (but necessary) hack due to the limitations of prior versions. At least with the LLVM toolchain, upgrades have been nearly issue-free so there is little to recommend not doing it. C++17 is a big upgrade over C++11 functionally. Developers that do not see compelling improvements probably weren't using most of the functionality of C++11 anyway.

While the number of features and quirks in standard C++ grows with each version, the subset that a developer needs to remember to write correct idiomatic code is shrinking with each new version as capabilities become more general and the expression of those capabilities simpler. There is a new, simpler, and vastly more capable language evolving out of the old one and you can increasingly live almost entirely within this new language as a developer. I hated classic C++ but I actually really like this new C++, and it allows me to write far less code for the same or better result.

There are still many things that C++ can express simply and efficiently that Rust, Julia, etc cannot. If you work on software that can take advantage of that expressiveness (e.g. databases in my case), the cost of changing languages is quite large.


> There is a new, simpler, and vastly more capable language evolving out of the old one and you can increasingly live almost entirely within this new language as a developer.

How is that true in practice? I would expect that once you have a codebase big enough, instead of living only with the new language you still have to deal with (or at least read) code written in previous versions of the language. And so instead of having to know only the modern subset you would have to keep in mind all the potential quirks from previous versions.


Sure, you will have code written in an older idiomatic style but there is some continuity, most C++17 programmers used to be C++11/14 programmers. Upgrading code from e.g. C++11 to C++17 is relatively incremental, typically easily recognized, and can be done opportunistically. Obviously going from C++03 to C++17 would be much more expensive, so that is a bit of a different case. Rapid improvements in code analysis often breaks previously clean builds in newer compilers, so you often find yourself doing hygienic work on old C++ code anyway.

This does imply some ongoing maintenance work related solely to keeping the code base consistently "modern". This almost always makes the C++ simpler and more maintainable and often reduces the line count, so it is worth the nominal investment. The massive improvement in template code readability is worth the price of admission all by itself.


Here is a proposal (from CppCon 2019) to create C++ epochs to drop support for old misfeatures in new code (inspired by Rust's epochs). Code written in the new epoch would still be fully backwards-compatible with older C++ versions and compilers.

https://www.youtube.com/watch?v=PFdKFoQxRqM



So you'll not only need to know all these sublanguages (epochs) but also how they interact with each other, especially when you need to refactor code that needs to cross these "epoch boundaries" (refactor the functionality not just to "upgrade" to a different epoch). Who comes up with that stuff? :-/


How does a developer who is new to C++ learn just the modern idiomatic style? Is there a "C++, the good parts"?




This has been my experience also. Of course you can't forget the old stuff if you're maintaining an old codebase. But, I'm grateful to use lambdas, std algorithm, if constexpr, and fold expressions instead of boilerplate classes, bespoke loops, SFINAE hackery, and recursive variadic functions. There is less wizardry necessary than in the past.


On the other hand, there aren't really any changes in 14 and 17 that break compatibility with code written in 11, at least that I'm aware of.

In the examples I've seen it seems to mostly come down to compiler, toolchain and 3rd party binary availability for the new versions, like with any major language version.

If for example you are using stock gcc from Ubuntu 16.04 for your builds, you can't use C++17.


For big (legacy) code-bases, just upgrading the compiler is a project by itself.

All 3rd party C++ libs need to be upgraded, all internal projects need to upgrade in an orderly fashion.

On top of that, just the change of the compiler will expose some pre-existing bugs.The bugs might have been there, but are not a problem for the business if they currently don't show.

I've seen projects with a core team of half a dozen people, supported by the other SW engineers working for 6 months just to upgrade to a newer version of windows and the compiler. This was mostly due to unwise SW-design decisions accumulated over 15+ years.


You raise a good point about newer compilers doing much deeper analysis of the code than older compilers. I have experienced the horror of compiling an old, nominally clean C++11 code base with the latest version of clang++ and seeing a small avalanche of errors and warnings that weren't picked up by the compiler when that code was written.

It creates a lot of work, and frequently the nature of the bugs are not material (i.e. in context they may not produce a runtime fault), but I do find it to be good hygiene. I used to do builds with two different compilers for the same purpose, though that found compiler bugs as often as it found issues in the code.


I totally agree, updating compilers is painful, and can be hard even for smaller projects. But for projects that went through that and actually got the newer compiler working with the C++11 code, I haven't yet seen anything that breaks compilation when then switching the compiler flag to C++14 mode.


Plus, a lot of new library and language features are simply standardized versions of features that mature codebases had their own solutions for years, so "upgrading" to a newer C++ may also imply a long process of deprecating these alternatives in favor of standard ones, and the subtle differences between the two can expose new bugs and issues.


Here is a list of some real-world examples of bugs that Mozilla had to fix when upgrading from Firefox from C++11 to C++14 to C++17 (still in progress). Many of these are compiler issues from upgrading to clang or gcc versions recent enough to support the new C++ versions, not C++ language changes.

https://bugzilla.mozilla.org/showdependencytree.cgi?id=15606...


Your link requires to be logged in to see the actual bug ticket.


Oops. Sorry! Here is a different view of the bugs. I verified this list didn't require Bugzilla account.

https://bugzilla.mozilla.org/buglist.cgi?bug_id=1396098%2C13...


> On the other hand, there aren't really any changes in 14 and 17 that break compatibility with code written in 11, at least that I'm aware of.

auto_ptr has thankfully be removed, but if you have legacy code that uses it it won't compile under C++17. Of course if you have code that uses auto_ptr you should fix it, but realistically nobody has the time for that.


Depends on how much it was used. But I was part of the effort to convert to C++17 for our (substantial) C++ code base of ~30 years and it was not painful at all.


I also ran up against the deprecation of std::mem_fun and std::mem_fun_ref in a recent migration. Easy enough to replace with lambda expressions, but one more hurdle to running C++17 code.


all compilers have ways to reinstate them to make porting easier, e.g. /D_HAS_AUTO_PTR_ETC=1 on msvc, -D_LIBCPP_ENABLE_CXX17_REMOVED_FEATURES=1 on clang...


I mean, if tricky usage of auto_ptr is stopping you, you could probably use a drop-in replacement. But the 'and this adds to the sprint tasks how?' question still isn't answered in a good way


You could probably just find/replace unique_ptr. I'm pretty sure that's what I did a million years ago when auto_ptr was deprecated.

C++17 does break some code by removing a few things, but they're pretty trivial and nothing sneaky.


clang-tidy has a command to replace auto_ptr with unique_ptr[0]. I haven't used it in anger, though.

[0] https://clang.llvm.org/extra/clang-tidy/checks/modernize-rep...


ABI stability of the STL is the biggest issue I've seen. Upgrading from C++11 to C++17 becomes a bust if any of your 3rd party libraries want to pass even a basic std::string.

I don't know what the hold up is on modules. Even a basic "this header uses a C++11 namespace" functionality would fix most of the problems.

But even better would be something like "extern 'Cwrapper'" that would let you expose C++ objects as a C API with a void* object pointer without having to do the pointless wrapper dance every time.

Like make the compiler convert "class Thing { doThing(int a); }" to "typedef void* Thing; Thing_doThing(Thing* this, int a);" automatically so that other languages can call me.


What's the issue with abi stability? GCC broke the Library ABI with C++11 but the old ABI was still available. I thi k it still available in C++14 and 17 mode.

If you switched to the new ABI with c++11 then c++17 as far as I know brings no new breakages.


No, std::string definitely changed between c++11 and c++14. It could be an implementation detail between different libstdc++ versions for the g++ version that supported c++14, but the incompatibility was definitely there.


> No, std::string definitely changed between c++11 and c++14. It could be an implementation detail between different libstdc++ versions for the g++ version that supported c++14, but the incompatibility was definitely there.

that is a problem with your distribution (and the reason why using distro packages sucks for development), not with C++.

The ABI only changes if you change the value of _GLIBCXX_USE_CXX11_ABI with libstdc++

See : https://gcc.godbolt.org/z/QLbjjo


Your example breaks if you change the C++11 pane to GCC 4.9 or earlier, which is the default in CentOS 7 and Ubuntu 14.04.

Considering that was only 5 years ago, I wouldn't consider this a stable ABI.


but again you can revert to gcc-4.9 behaviour with -D_GLIBCXX_USE_CXX11_ABI=0


I don’t understand why they don’t do more about ABI. Compared to C# or java it’s such a pain to share code in C++. To me this would open up a lot of opportunities to create more libraries.


ABIis hard because C++ structures are typically more complex than C structures. While the C documentation could simply include the struct (and the ABI would constrain padding, etc), consider a simple std::string, which may (OK, will) have small string optimization: different runtime libraries might make different tradeoffs as to how to do this. You'd have to constrain the entire standard library for your platform which could forestall improvements down the road. The committee already unwittingly made that mistake with std::map and doesn't want to make that mistake again.


C++ also insists on inlining code extensively across the provider/client boundary, so if the offset of any data element changes the binary library is no longer backwards compatible.

This is exacerbated by the lack of usability in compilation toolchains that promotes header-only libraries just for build simplicity and for which the data structures often won't be compatible across .so boundaries.


Not dissing your comment, just pointing out that the committee is not just aware of the problem but slowly doing what it can to address them. C++17 and C++20 have a bunch of features to improve both of these factors.

There are also a number of features not really intended for user code designed to help make libraries faster, and more stable. The downside of course is, well, just look at library source. But really the source to the C standard library has similar issues.

I don't like header-only libraries because they slow down compilation, and slowly the trend is starting to abate.


They do. ABI breaking changes are routinely voted down unless they are really worth it (and the only non backward compatible change that was accepted that I'm aware is the std string changes in C++11).



Thanks for the link, a good read.

As somebody who kicks C++'s tires once in a while and is always put off by the incredible complexity of the language, and the complexity and friction and fragility of its workflow, I was sad not to see any explicit mention of pairing an ABI break with removal of the bad parts. (Yeah, I know, that might mean source-level backwards incompatibility...) Makes me wonder if it's ever even discussed at a high level.


Maybe things are better now but in the past almost every new library I added to my projects required a lot of fiddling with headers , link options and other stuff. With C# this is so easy in comparison.


Sounds like a job for an llvm tool. The parse tree is exposed, doing the transform shouldn't be ridiculous? Are you keen enough?


Re (5), I rather feel that the quality of the documentation and communication around modern C++ puts everything else to shame. If you knew java or python in 2009, how are you supposed to quickly catch up with 2019? (Rust also seems to have piled on a lot of additions since 1.0, but I don't use it so I shouldn't speak.) There are books and blogs for everything popular, but it's really a little shocking to me that no other big language has anything that can compare to cppreference.com.

This doesn't change what people other than me might be thinking, of course.


I guess it's a question of habit. I'm learning C++ during my free time since a few months. I find cppreference.com really difficult to parse. I understand way more of the documentation now than when I started because of the bit of experience I gained, but it is written in a way that makes everything sounds really over-complicated. I remember when I googled for something and arrived at the page about "Value category": https://en.cppreference.com/w/cpp/language/value_category. If you're not already well familiar with the language, this page is really hard to understand IMHO.

I found it way easier to deal with Go, C#, and Ruby documentation for example.


I don't recommend cppreference for novices. It is by no means an easy read. It is a reference, first of all. It enumerates all possible minutiae exhaustively. Unless you are already familiar, to a degree, with most of the topics, reading it is going to be frustrating. The second obstacle is the terminology. C++ defines a lot of terms that are not in common use in other languages. It takes time to be acclimated.

The reason that it is popular in the C++ community is that it cuts down legalese a lot when compared to the language standard.

Try books first. Meyers and Josuttis write excellent books. The template guide by Vandevoorde and Josuttis is my gateway drug to becoming a language lawyer. It is still hard, but at least manageable.


> The following expressions are lvalue expressions: [...] a string literal, such as "Hello, world!"

That's pretty interesting. I'm not even sure how to test the effect of

  "Hello, world!" = <something>
I imagine the reason this is allowed has something to do with how strings decay into pointers.

> I found it way easier to deal with ... Ruby documentation for example.

Maybe in documentation detailing the methods available, but as far as I know, there's no official spec for the syntax nor the details of the semantics of the language.


Cppreference is great, but it is not the best place to learn the language.


I agree that cppreference is amazing and is my only c++ link in my browser favourites. Documentation is typically clear and succinct. And its a wiki!

Not a fan of the spongers that scrape it though.


(1) It's actually rather sensible, because C++ is famous for its backwards compatibility. So you can use C++14 or C++17 features in some new code while leaving the rest as-is - and gradually refactoring some of it with new features.

(2) This used to be the case with C++03 (or C++98), and it changed...

(3) I partially disagree and partially claim you're begging the question. You don't have poll results to suggest what people think of C++14. But - C++14 had a lot less changes compared to C++11 or C++17; this was natural considering how the cycle of debate of potential features progressed.

The thing is, the fact that C++14 wasn't that revolutionary in terms of features also means it is easier to adopt. If you want more novelty - try C++17; or - try a C++14-dependent library like Eric Niebler's ranges-v3.

(4) C++17 allows for writing less complex code in many cases. Specifically, you can significantly reduce the need for template metaprogramming - a particularly hairy part of the language - by using more constexpr functions and if-constexpr in regular functions. And there are other examples. Like Bjarne says: The C++ committee endeavors to "make simple things simple (to write)". So it's a trade-off.

(5) Some people switch languages because of workplace decisions or personal preferences, that's true; but people also adopt C++ because of what becomes possible, or easily-expressible, with newer versions of the language. For example, using the ranges library and with C++14 or later, functional programming with C++ has become quite reasonable, borderline pleasant even.

---

Bottom line: In my opinion, at this point C++11 has more inertia than momentum. And that's ok. People will progress over time.


(1) I know of one problem with C++17 and old code. C++17 removed throw specifications from the language (http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p000...) . We use third party libraries that contain these which keep us from using C++17.


This feature was already deprecated in C++11...

Also, this is an "easy" breakage, in that it was glaringly obvious. That is, you didn't encounter a strange compilation bug and had to investigate why it occurred, or got different run-time behavior of the same code.


(4) is my main reason for missing C++17 when I am on a project stuck with C++11.

I feel that the template metaprogramming framework deserves an overhaul to clean up historical mistakes and keep only the good part.


In C++ there are almost never any overhauls - it's almost always new functionality that can replace older functionality. That's what compile-time evaluation is to template metaprogramming.


> There's no widespread perception that C++14 or later bring compelling language improvements.

I would argue the opposite: C++11 is a fairly different paradigm to C++03, and upgrading an existing code base is a sizable project.

But once you get hooked into the concepts it introduces (auto, lambdas, templates, constexpr), each subsequent revision of the language standard introduces some improvement to these concepts that deals with unpleasant edge cases the earlier revision had, so each of them promises to make your existing use cases simpler, at a moderate rewrite expense.


C++11 vs C++14/C++17/C++20 is not python2 vs python3. Because of the emphasis on backwards compatibility, you can often just recompile your C++11 project with the new version of the standard and everything should just work. Backwards compatability also means that the newer versions are only as "complex" as the features you choose to use.


The tricky part of this is that if your project is a library, you're not just opting into having to update your own toolchain, but also that of all your users. This means that a lot of library developers will err on the side of not updating their minimum version, which in turn means application developers don't have as much pressure to update either. This isn't specific to C++ by any means, but I think it does pop up a bit more in this space due to the fact that a lot of developers prefer to just use the default toolchain of their system, so forcing downstream users to update their toolchain could be viewed as a bit more of a maintenance burden.


> (3) There's no widespread perception that C++14 or later bring compelling language improvements.

Constexpr if (introduced in C++17). If you're building a library that uses templated types (and therefore static dispatch instead of virtual functions and dynamic dispatch), then using constexpr if dramatically simplifies the implementation of your code.


I don't think GP was arguing that there weren't any compelling language improvements, just that there isn't a "widespread perception" of those improvements.


> here's no widespread perception that C++14 or later bring compelling language improvements.

C++14 didn't add a whole lot of new things; it mostly improved on existing features.

C++17 adds a number of new things.

I would use it just for std::optional and std::variant though.


Do you think that will change with introduction of modules? Would that be a feature compeling enough to make people move to newer versions of the standard?


They will switch to 20 for modules. No. 1 headache is build times. It will literally save money.


C++14’s compelling improvement is initialization in capture lists, so you don’t have to copy into them. I haven’t seen anything compelling beyond that.


Right now my biggest things I like from C++20 are:

- Concepts (Now I can use static polymorphism without all those SFINAE template hacks)

- Designated Initializers (Finally!!! Using POD structs became way more convenient now)

- Coroutines (Would be pretty nice for writing games / interactive applications)

The things I don't have any interest in (but don't care if it's in or not)

- Ranges (Too much complexity for something you could do with plain old for/if loops...)

The things I'm worried about:

- Modules (Theoretically really good, but in practice the whole thing is starting to become a mess, especially when it's interacting with the preprocessor and external libraries, and when trying to preserve backward compatibility.)


I have hope for modules because they can improve build times. Not exactly sure they would improve build time but if it's true, it would be a massive improvement.


Meanwhile, I am deeply concerned about modules, as it seems like the kind of feature that is going to massively harm build times by making it much more difficult to do truly independent distributed builds (which right now is pretty easy with C/C++ due to its separate compilation and extremely explicit inter-file dependencies).


Modules make separate compilation possible. C++ does not really have separate compilation right now.


I do not understand this comment, as I have done separate compilation every day with C++ for over two two decades: the model inherently only allows this, as every translation unit absolutely must be compiled separately and then linked... you don't even have a choice in the matter. I have also used both off-the-shelf and custom distributed build systems (most recently, I have deployed one which does insanely-parallel builds of a C++2a codebase using AWS Lambda), which pair extremely well with the explicit file-oriented dependencies that can be reported by each translation unit and used to determine which parts of the build are now stale given changes to the filesystem. I have never seen languages with module systems support even basic separate compilation, and it terrifies me; meanwhile, the flags I have seen being added to clang for attempting to preserve separate compilation behavior with the modules spec doesn't leave me feeling good about the endeavor. It could be my fears are unjustified, but it is extremely strange and somewhat annoying that the advocates for it don't ever seem to appreciate the state of the art for the status quo.


> I do not understand this comment, as I have done separate compilation every day with C++ for over two two decades: the model inherently only allows this, as every translation unit absolutely must be compiled separately and then linked... you don't even have a choice in the matter.

Except if your headers include macros or templates, which must be evaluated every time. This is not separate compilation. What C/C++ developers have been doing for 20 years is jumping through hoops to incrementally reuse previously compiled results because C/C++ violate basic modularity principles [1]. They call it "separate compilation", but it's a pale shadow of true separate compilation.

Proper modules would enforce abstraction boundaries so true separate compilation becomes possible. I can't speak to the specific C++ proposal here, but enabling separate compilation is exactly what modules are for.

[1] http://llvm.org/devmtg/2012-11/Gregor-Modules.pdf


Normally use of templates do break separate compilation but it can be recovered, and it is routinely done on large codebases, via explicit template instantiation. The ergonomics are not great of course and hopefully modules will improve things.


With C++, you have a lot of limitations on cross-machine, cross-library compilation. The library is not defined just by the platform, but by the compilation directives and macros.

Since headers can redefine macros, that also means that even including headers in a different order can change the meaning.

Older languages like C++ with headers are simply source files with a different extension, so your individual source file compilation is actually loading in and parsing cross-sectional subsets of your full project. Modern languages typically define the interface inline with the code, and export that interface to other modules which would consume it. The reason that you don't have intra-module distributed build is because the intra-module code dependencies are implicit - there is no developer-defined partial cross section of the code, all of the code needs to be pulled in and semantically evaluated as a unit.

You could hypothetically still do distributed optimization of the code after it is semantically evaluated, but TBH there is a lot of time wasted in evaluating headers for each file. Modern compiled languages usually have compilation time as a restriction on the implementation and design, and elimination of the need to repeatedly parse and semantically evaluate files is part of their design.


> The reason that you don't have intra-module distributed build is because the intra-module code dependencies are implicit - there is no developer-defined partial cross section of the code, all of the code needs to be pulled in and semantically evaluated as a unit.

And this seriously somehow doesn't seem like a problem to you? My massively distributed (to AWS Lambda) C++ build compiles at the speed of the slowest single translation unit. You seem to be optimizing for serial compile performance, but if you truly want speed you have to go parallel. These explicitly-managed "cross sectional sunsets" are what makes this so easy to do with C++.


There are definite tradeoffs here, but one thing to consider is that dev tooling generally can't benefit from that kind of massively distributed parallelism. If you make a change to a widely-included header file, code intelligence tools suddenly have to do a huge amount of work to reevaluate their internal model of all affected translation units. This can really result in an unpleasant and laggy editing experience, even on medium-sized projects. That's the kind of thing that modules can help with; I certainly wished fervently that they were already a thing when I used to work on C++ static analysis tools years ago.


You can still do distributed builds with modules. You can't have cyclic dependencies anymore though. Your code has to be structured into logical modules that are meant to be built in parallel. It's not going to be free. You could have replicated modules with the old preprocessor style, but this way it is formalized and enforced by the compiler.


Wasn't the point of modules to decrease compilation/build times?


Some claim this, but I haven't seen it play out this way yet in what I have seen for modules; it could be that people are hyper-optimizing for serial performance when the future (and present!) is parallel? If so, :(.

This article is possibly out of date--or maybe was wrong to begin with--but it shows how the modules concept from earlier this year simply broke parallel build efforts by enforcing a ton of serial dependencies.

https://vector-of-bool.github.io/2019/01/27/modules-doa.html


I have also seen this. My guess is that it highlights more that there are way too many dependencies in the libraries being used and that those dependencies aren't organized well.


Have you tried using IncrediBuild. It's a dedicated solution to reduce C++ build times. If you're working on Windows, you can use it free (sharing the link to download): https://www.incredibuild.com/ibonlinestore/register#/regWind...


OTOH ranges usually get compiled to faster and more efficient code. Smaller too.


> OTOH ranges usually get compiled to faster and more efficient code. Smaller too.

than raw loops ? do you have an example ?


Not particularly. However I do develop on OpenWrt where size is important. On some C++ projects, when I convert the for loops to range based ones using clang-tidy's modernize-loop-convert, I usually get a reduction in compiled size (compiling with -Os of course).


If think the parent was talking more about C++20 ranges based on the range-v3 library, not about the for loop ranges.


that is indeed the case.


Designated Initializers done awkwardly a.k.a. "the C++ way"

  // Point2D point2d {.y = 2, .x = 1};         // (1) error
Oh my. The C++ committee keeps their flight altitude rivaling moles and worms. This is another brittle feature, just like the initializer lists that have to mirror the declaration order. In what world is this a better option than allowing any order? Can't compiler implementors be bothered or what is the rationale?? C does this better.


I think you are confused about the intention. If you want order independent initialization you could just as well use the old initializer list. The idea here is that you DON'T want to accidentally initialize x with y.


I don't think I am confused. The point is that, logically, either you do not tag something with names, and then, naturally, the order decides. Or, you tag something with names, and then, given you named things, there ought to be no order. Not so with the C++ initializers. An initializer list in a constructor should mirror the declaration order, or you won't go warning free. Every time you'd touch the internals of your class, you'd need to touch every constructor that uses an initializer list. Same with the struct initializers now. You are having this:

  struct A { int a, b, c};
you need to do this:

  A a = { .a = 1, .c = 3};
but you can't do this:

  A a = { .c = 3, .a = 1};
because... well, because C++. There is no reason to disallow the second form except for .. what exactly? What is the reasoning of the C++ committee to not be able to mirror what one can do in C (where, aside of the missing typedef or "struct" in front of A, both forms are valid)?

Just as you're saying, the C++ committee seems to be confused about order-dependent and named initializations, and for some love-of-bean-counting insist that named initialization needs to respect order _as well_.


There is of course a reason which is well documented in the papers. Constructors, and initialization expressions can have side effects in C++. An invariant of the language is that member constructors are always invoked in declaration order, so if you were to swap the initializers around, the compiler would still have to initialize them in the original order, which would be surprising.

Which does not means that it couldn't be done, in fact constructor initializer lists have the same issue and you are allowed to list them in any order but this is considered a language mistake and most compilers warn about out of order initializer. This mirror the arbitrary evaluation order of function parameters which is also considered a mistake.

In the end there was no consensus to allowing an arbitrary order, but in the future that can be relaxed without breaking any code if a consensus is reached, while the reverse could not be done.

As usual it is always compromises.

Anyway, at last for me, initializers are more about writing more explicit code and be more robust in the face of changes.


> There is of course a reason which is well documented in the papers. Constructors, and initialization expressions can have side effects in C++. An invariant of the language is that member constructors are always invoked in declaration order, so if you were to swap the initializers around, the compiler would still have to initialize them in the original order, which would be surprising.

The following:

    Point2D pt = {.y = 5, .x = 6};
should not have meant that 'y' is initialized before 'x'. It's only a lexical convenience that exists before parsing. When the code is parsed into an AST, the actual AST would reflect the following code:

    Point2D pt = {.x = 6, .y = 5};
There is absolutely no reason to enforce declaration order.


I tend to agree with ephaeton that forcing the order is a little heavy-handed. But thanks for the insight about initialization order.

For what it's worth, I find strictness pedantry like this fairly useless in production. For example, a JSON file has an order to the elements of an associative array, but Javascript doesn't. So most of the time, I treat associative arrays as having no order, and use another array to map indices to keys or whatever.

Contrast that with PHP, which does maintain key index in the order it's added. There have been countless times where it turned out that I needed that order, and it saved me the work of declaring the extra array. Even more importantly, I've never been hit by that surprise (of not having order) in PHP, whereas I have in pretty much every other language.

So maybe PHP is less pure, but truthfully, pureness is the single biggest source of friction in my daily life. Basically the entirety of my day now goes to setting up environments from minimalist environments, working around subtle language errata, or translating what I want to do in my head to the domain-specific language decisions that are so widespread in languages like Ruby.

I prefer the lazy route now in most things. Just give me everything, with the least-friction needed to do what's in my head, and let me prune it/optimize it when I'm done. So in this case, I'd prefer C++ to have a strict ordering of elements, but let me initialize them in any order, so that I can think in abstractions rather than implementation details. Which is why PHP is roughly 100 times more productive for me than C++, even though I grew up with C++ and know it like the back of my hand.


Features I wish C++20 had:

* An opinionated, modern packaging and dependency story (like go modules, Rust crates)

* built-in library support for logging, http, zip, gzip, json, yaml, template rendering, RFC3339 datetime reading/writing

* the dream: compliant compilers must be able to compile down to static binaries, cross compiling built-in.

Features C++20 actually has:

* new fancy spaceship operator that I'll now have to learn and never use...


> built-in library support for logging, http, zip, gzip, json, yaml, template rendering, RFC3339 datetime reading/writing

That's completely out of scope for a programming language pretending to be taken seriously as a general purpose one (including e.g. systems programming).

Also, your list looks like a pretty arbitrary choice of protocols and formats, why don't we also add support for SSH, SMPT, IMAP, PNG?. In the end, everyone would want their niche protocol/format to be included in the standard.

> the dream: compliant compilers must be able to compile down to static binaries, cross compiling built-in.

Cross-compiling to what architectures specifically? The most used ones are proprietary and I think that disqualifies them from being included in a serious international standard, since you'd need to read the spec and implement it in order to be compliant (I'm not versed in ISO inclusion requirements, so this may be already happening, but it would still be wrong. (grepping "x86" in the offical C standard returns no results though.)).


> built-in library support for logging, http, zip, gzip, json, yaml, template rendering, RFC3339 datetime reading/writing

There is a Boost library for each of those things.

> An opinionated, modern packaging and dependency story (like go modules, Rust crates)

> the dream: compliant compilers must be able to compile down to static binaries, cross compiling built-in.

You can use Conan or vcpkg. LLVM basically solves the cross-compiling issue since you can take the IR to any platform that has a LLVM target.

Neither of these are feasible to include in the International Standard because C++ runs on more than amd64 and would make a lot of obscure platforms no longer compliant with the standard. Rust crates are nice, but people building medical devices with C++ shouldn't need to worry about supporting an npm-like monstrosity in their builds.


LLVM IR is not platform independent.

Rust compiles on far more platforms than amd64, and Cargo works just fine.

Nothing forces you to depend on any packages you don’t want to.


> Nothing forces you to depend on any packages you don’t want to.

Nothing stops you, either, which is a problem for everyone in the future who uses your code. The first reaction to any problem in JavaScript/webdev is to google for a library to import to solve the problem. This is acceptable behavior because the existence of a web browser implies powerful enough resources to accommodate bloat. But this mentality isn't acceptable on other systems and has even infected some basic functionality:

https://crates.io/crates/rand

The random number generator for Rust has 6 dependencies. In C++, it's only lib[std]c++. In Java, it's java.base.


Random number generation has trade-offs for speed, security, and thread safety.

Most other langues with a "simple" built-in are unfit in some of these aspects, so you may end up with a footgun and still have to find a replacement.

In Rust you can choose which algorithms are used and how. And because it's a regular package, it can be improved without breaking the language.

Note that number of dependencies doesn't mean anything. Some crates are split into tiny deps for each feature, so that you can reduce binary size and compile time if you disable optional features.


That's true for libc's naive rand() which is used by Python, JS, Ruby, PHP, etc.

But stock C++ has many options for the random number generation algorithm:

https://en.cppreference.com/w/cpp/header/random

As does Java and .NET. Without any dependencies and guaranteed to be portable.

> Note that number of dependencies doesn't mean anything. Some crates are split into tiny deps for each feature, so that you can reduce binary size and compile time if you disable optional features.

In theory I agree. It just seems like one of the persistent unsolved problems in software is dependency hell (spawning the whole containerization movement), so I'd like to avoid it if I could.


C++ has many audiences with disparate needs; ranging from firmware, to scientific computing, to application development.

Most of the industries using the language optimize their code for a target platform. And general non-optimized portable binaries are simply not cost effective, so standardizing installation has not been simple.

If you do care about portability, you functionally need to. - ensure you're not linking to anything but LIBC and the VDSO

- compile with no assembly language

- disable most optimizations, and select a sufficiently generic hardware target. For the most part, they succeed in features.

- directly compile all 3rd party source in your binary.

Then, your binary should last a long time.

That all said, I believe your needs are in the minority in the C++ community, and other languages likely better suit your needs.


i never understood the need for package managers. just check your dependencies in and build with the rest of your source code. this doesn't introduce extra moving parts into the system (why should i need an internet connection to build my stuff?), easy to patch and debug dependencies, gives you full control and doesn't restrict your toolchain (e.g. conan doesn't as of now support vs2019+llvm). what i _would_ like to see though is a standardized build system for c++ projects, although that will likely never happen.


This is what most companies do irl for security reasons. It's also the easiest, most obvious solution with the least moving parts.

There will never be a standard build system, but CMake is the de facto standard build generator and works well enough.


On thing I appreciate is mdspan -- multidimensional projections onto a regular linear vector. I've always had to spin these myself which means they don't fully participate as ordinary containers (who's going to go tho all that work for their own application code).

I'm hoping I can map them into GPU space -- looks like the standard has adequate control for this.


Does Eigen not work for your use cases?


That's a good call for a great library.

It's often overkill for what I do, especially as it can't use the same compiler options as my tree normally does (they have to bend over backwards for back compatibility; I don't). I do use it some places. I certainly don't envy the hard work of library developers!

When I simply need a 2D or 3D resizable array it's often easier, as I mentioned, to simply spin one up. Having that support "native" will be great.


mdspan didn't make it in 20


Rats!


I've been anxiously awaiting modules and concepts. Both are incredibly important to reduce developer burden and improve the development experience.

Modules: While it is important to be able to fine-tune linking and compilation in some settings, in most, and especially for beginners, this should be handled by the compiler. Especially when compared to other modern languages, C++ is much more complex to understand the toolchain and what's going on under the covers with the dichotomy of compilation and linking. Header files were a hack that have been around for too long, and there needs to be less separation between the interface and actual binary code when compiling. This is a headache both for novices and the experienced.

Concepts: The missing link for templates. Besides removing some of the roundabout-ism of SFINAE by providing compile-time verifiable interfaces, I think the biggest benefit of concepts will be the error messages. Right now, the state of errors in heavily templated code is abysmal, and this translates to a lot of wasted time for developers. Concepts should allow the errors to indicate exactly what's wrong.

I can't wait to be able to use these in public code. Some compilers support concepts with a flag already (and GCC 10 has it without the special flag), though none support modules yet...


Could someone describes the keyword "unlikely" and "likely" in a bit more details? It seem to be a very niche thing to add to the language. Is it expected that those hints will have an impact important enough regarding what the optimizer can do?


likely and unlikely exist in different forms as compiler extensions for a while. There is code where they indeed help compilers to produce slightly better code for the hot path. Good use typically involves lots of measurement as developers are often wrong about the impact and about what the real flow is.

What kind of things can the compiler improve? For instance it can arrange the code in a way that the instructions of the hot path are directly behind each other, whereas the unlikely case jumps further away. Or it can arrange code a bit to help the branch predictor.


gcc and clang (at least, I don't know about msvc) have had these for a while. Basically, the instructions for whichever branch is declared most likely to be taken will be placed immediately after the comparison instruction(s), which is generally more cache-friendly.

It seems conceivable that it might also affect the compiler's inlining choices in each branch (i.e. don't bother to inline as aggressively in the less likely branch, to reduce code size), though I don't know for sure.


Intel and AMD don't hasn't any hinting instructions. Mostly it will affect inlining and well move unlikely code away so the likely path in straight.

But is can already figure that out often especially if it sees exceptions.

Better is to just use the profile guided optimization in the compiler.


A common way to extend a language is to standardize what people are doing in practice. It seems niche, but you have to remember what C++'s niche is. Linux kernel etc use hints like this extensively


I thought the Linux kernel was C and not C++. Has Linus changed his opinion of C++ in all these years?


Features needed/useful for kernel development often get implemented as extensions to gcc and other compilers and then later standardized.


In some niches it is important.

Say you are writing a high speed trading program. At some point you get to the "if(wouldMakeMoneyOnTrade())...". However for every time this passes and you trade there are 1000 where it fails and you don't. However time is critical because your competitors are using similar algorithms and so you need to beat them - thus you put in a likely and ensure that the CPU jumps to the trade code the fastest possible. Your competitors that use Java pay a CPU branch prediction penalty here because the hotspot optimizer has noticed that the if almost always fails and optimized for the false case. Thus you can make money by beating your competitor to the trade. (Note, this doesn't mean you can't use Java for your code, but if you do you need to manage that risk somehow)


If you don't use FPGAs, you probably lost that game anyway.


A stop-the-world GC for HFT doesn't sound like it would work all the well…


Many HFT firms use C# or Java. Quite a few matching engines are written in those languages as well.

The trick is to remember that garbage collection is not some sort of werewolf that wakes up and decides to mess with your stuff whenever it feels like it; it (can) happen only at certain, documented points. So you have to write your code in such a way that it does not do memory allocations in the critical path (which you would not be doing in C++ anyway, for a similar reason; malloc/free is expensive).

Generally if you're doing HFT your critical path is not going to being doing much anyway. A few reads, comparisons, maybe some trivial arithmetic and that should be it.


For what is worth, at least in London, all the HFT firms I have worked with or interviewed for use C++. As I'm a C++ programmer there is bias of course, so my sample is probably not representative. Still I suspect that those that use anything other than C++ are a small minority.



Interesting. From the comment in abseil's implementation:

// Compilers can use the information that a certain branch is not likely to be

// taken (for instance, a CHECK failure) to optimize for the common case in

// the absence of better information (ie. compiling gcc with `-fprofile-arcs`).

//

// Recommendation: Modern CPUs dynamically predict branch execution paths,

// typically with accuracy greater than 97%. As a result, annotating every

// branch in a codebase is likely counterproductive; however, annotating

// specific branches that are both hot and consistently mispredicted is likely

// to yield performance improvements.


Without actually looking at the docs, I would guess these map directly to cpu intrisics that can be used to guide the cpu branch prediction algorithms.


Which cpu instructions guide branch prediction algorithms? Which cpus?


At some point pentium4 had static branch prediction hints, but they are obsolete and no longer relevant.

Today __buitin_expect are mostly used to mark hot/cold branches. The compiler will normally optimize hot code for speed and cold code for size also will attempt to put cold code in separate cachelines and even pages. And other similar code layout optimizations.


Been away from C++ a long time. If picking it up again, is it better to just start with C++17 or 20 or stick to C++11 which seems to be the defacto production version?


Depends on your needs. If you don't have a lot of or any legacy code yes, go straight to c++17 (or even 20 though you won't be able to use many of the features right out of a the gate, or without third-party libraries). If you can do this you'll be glad you did.

I was able to start a project in c++17 in 2016, though at the time I had to use boost::variant with the Xcode compiler and boost::filesystem for all three compilers (only a limited use so I could even have skipped it). Also for some complex third party libraries I had to make a little trampoline file that compiled in C++14, or with special flags, and then call those entry points.

Since I was starting from a blank slate I also compiled everything with pretty much maximal warnings an -Werror which really improved the code a lot, something you can never get away with with legacy code (not to insult working legacy codebases!)


C++11 was the major version bump. It changed how you're supposed to write C++. C++14 & 17 are just minor fixes & improvements. So there's really no reason to not start with at least C++17.


I haven’t done C++ a while but I really like where this is going. It’s a complex language for sure but I always liked about it that whatever needed to be done the language would not get in the way. There is something to be said for working in the language your OS and a lot of other libraries are written with. You know that you have high probability of achieving what you need to do. It may need a lot of work though.


With consteval (and its cousins), std::source_location and modules you could pretty much compile C++ without the preprocessor. Hooray!


There's still the need for logging and assertion functions, where you want to test the debugging level or some other value before you evaluate the arguments to the function. Right now, you need to use a macro, but there's a proposal:

James Dennett and Geoff Romer. P0927R2: Towards A (Lazy) Forwarding Mechanism for C++. ISO/IEC C++ Standards Committee Paper. 2018. url: https://wg21.link/p0927r2.


Hopefully between consteval and string literal template parameters, we'll be able to detect literal const char* in templates.

Would be nice for my threaded logger to know if it can copy the pointer instead of the whole string, knowing it was initialized statically.


Wouldn't the ideal solution involve the const system?


Is there a good "tutorial for modern c++" resource out there? The last c++ I used heavily was c03 and a sprinkle of c11. I find reading modern c++ to be somewhere between frustrating and an exercise in hieroglyphics thanks to how much it's changed. Something along the lines of "basics of why to use the different std pointers/views instead of raw pointers", lambdas, all the new const-like bits, and whatever else have become "core" features.


I've read "A Tour of C++" by Stroustrup. It was helpful in trying to catch up to modern idioms for C++.


Scott Meyers's books, although I think they stop at C++14 will get you up to speed


I'm not sure I grok the `<=>` operator and the example they show seems to just show that `a == b`. Can someone explain what's going on with that?


Previously you need to override all six operators (==, <=, <, >, >=, !=) to provide total order comparators. Now you only need to provide one method.


> I'm not sure I grok the `<=>` operator

Think of `strcmp` which returns an int. The int can be <0, ==0, or >0. That same `strcmp` can then be used to implement all other standard comparison functions. `operator <=>` is the generalized version of that.


An important part there is: Most users will never call <=> themselves. However somebody implementing a type can implement a single function and the compiler will translate usage of <, <=, ==, !=< >=, > to the single function. Reduces boiler plate in the implementation. (Think about it - most implementations of > or < will often use the same logic already, spelling this out is "annoying"; mind that for performance reasons it can still be worthwhile to implement a == explicitly as equal comparison is often faster than ordering, but that's something the type designer can chose)


> Think about it - most implementations of > or < will often use the same logic already, spelling this out is "annoying"; mind that for performance reasons it can still be worthwhile to implement a == explicitly as equal comparison is often faster than ordering, but that's something the type designer can chose

Yup. My C++ code would often include a `int T.compare(const T& a)` member function. Then any comparison operator would be custom written too -- but only to delegate to the custom compare and check its result.

This one feature alone will eliminate several lines of boilerplate.


Do you have an example of when == would be faster than a comparison?

I can't think of any case that would have any appreciable difference in speed.

Is it mostly around eliminating branching logic?

Wouldn't there be a good chance that might be inlined and optimized away anyways?


On case is string comparison. When implementing == I can shortcut if length doesn't matter the string can't be equal. When implementing <=> I however have to iterate in any case. (While there the question then is if the shortcut is really efficient, as the iteration might already stop on first character ...)


Htfy96 is right. This post gives more examples. https://devblogs.microsoft.com/cppblog/simplify-your-code-wi...

Along with automatic synthesis of all the operators, c++ 20 also clarified rules about overload-matching and automatic construction of objects for purposes of comparison, so you don’t have to write the friend methods to for example compare int to your type if you have trivial int-taking constructor for your type. Also if your == operator or != is more efficient than a full lexigrapgical comparison the compiler will now do the right thing


This book is worth checking out if you don't know c++17:

https://www.bfilipek.com/2018/08/cpp17indetail.html?m=1

That guy has a great blog where he writes at length about each feature, and the book is a compilation of that.


Some parts of C++20 are really good, like most of what is stated in the article.

But C++ would not be C++ if it had no shenanigans coming. Unless they have changed things within some months the new calendar/datetime library is hilarious.

It has features like being able to write a date like: 1/2/2000y. ooh fancy operatoe overloading. Which one is month? The first one ofcourse as it they didn’t standardize on any of the existing ISO standards, or allow you to choose, but selected ”Customary US way” as the Only True C++ Way(tm).

Also do not forget the abuse of operator overloading like in the good 90’s when misusing it was the recommended thing.


FWIW the absl CivilTime library is excellent: https://abseil.io/blog/20181010-civil-time


As far as I can tell from cppreference (I literally found out about the datetime extension to chrono right now), your example won't come mobile because it is ambiguous.


Why not something like Date.of(2000, 2, 1)? Much more sane. Or Date.ofYear(2000).month(2).day(1) for more descriptive usage.


Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: