1. 10
  1.  

  2. 2

    My favorite from this list are constrained auto and the mathematically correct comparison functions (I’ve periodically inherited code bases that were scary to change but would produce many many signed vs unsigned warnings when actually compiled with warning enabled)

    1. 1

      using enum makes me very happy. I can finally write code like this:

      struct Foo
      {
              enum X { A, B };
              using enum X;
      };
      
      Foo::X x = Foo::A;
      

      Previously, X was its own symbol namespace and so this the last line ended up needing to be:

      Foo::X x = Foo::X::A;
      

      If the enum has a useful and meaningful name, then this is incredibly verbose. The class can avoid name conflicts (don’t declare two enums with values with the same name in the same class, it’s just a bad idea) and they remain scoped to the class.

      I’ve already used non-type template parameters to use strings as template parameters and even pairs of string and value to define compile-time maps. These are really fun.

      Similarly, I’m already using the structure initialisation syntax to provide optional named arguments to functions.

      I’m a bit sad that it’s taken 30 years to get a starts_with and ends_with method on strings but strings in C++ are still a bit of a mess. Baking the representation of strings into its interface is one of the biggest mistakes a language / standard library can make and it’s very hard to fix that in C++ now.

      contains is similar. It’s the most common operation that I do with any set data type, and with std::set I need to do a lookup and then compare the iterator against end(), which is verbose and not instantly obvious what it means.

      Heterogeneous lookup is also something I’ve been missing for a while. Objective-C lets you look up using any type that implements the compare methods correctly. This is somewhat error prone because it’s hard to ensure that [a isEqual: b] == [b isEqual: a] in the presence of subtyping. The C++ approach is much cleaner and it lets me do things like have an owning object (smart pointer, wrapper around a file descriptor, and so on) in the container and use the non-owning variant for lookups. I have some slightly convoluted code that works around not being able to do this today, which I’m looking forward to updating to C++20.

      I’m also using the source location stuff already in some custom invariant-checking functions that use libfmt to provide pretty error messages when I hit an invariant violation (the annoying thing about C’s assert is that it can tell you x != y but it can’t tell you what x or y is).

      As with C++14 and C++17, there’s nothing there that is completely revolutionary but lots of things that each make my life a little bit easier.

      Oh, and one more thing: std::atomic now has basic futex-like behaviour. Unfortunately, I didn’t notice that the implementation in libc++ is far from ideal until we’d shipped a binary release and now it’s part of the ABI. Microsoft STL does it a better way. This should let simple futex use cases work without any platform-specific code.

      1. 2

        what was the issue with libc++’s atomic?

        1. 1

          For std::atomic::wait and std::atomic::wake, there’s an interface between the code in the header and the library. This interface is the simplest case for something like a futex.

          Most operating systems have something like a futex, at least for this case: in the wait path, the kernel acquires a lock indexed by the object’s address, compares the object’s value against something provided, and (if they don’t match) releases the lock and blocks the calling thread. The corresponding wake call acquires a lock indexed by the address (so, the same lock as the wait), signals all waiters, and returns. Userspace code on the wake path does an atomic exchange to read modify the value and see if there are waiters and then issues the wake if there are any waiters. If a waiter races then it will either fail the compare that the kernel does with the lock held and not block, or it will wait just before the wake.

          The C++ atomics model requires these operations to work with std::atomic<T> for all values of T. This means that types that the platform’s futex equivalent does not natively support must be implemented by maintaining a local table mapping from address to a lock word. The interface for deciding when to use the underlying value directly with the kernel interface or use the look-aside table forms part of the ABI contract between the header and the standard library. In libc++, there were two poor design choices:

          • The header defines a single type that is used natively as keys. This is a 32-bit integer on Linux, a 64-bit integer on all other platforms.
          • The header uses type-based overloading to dispatch to this version.

          This is fine for Linux today, where futex supports only 32-bit keys and for macOS where the equivalent supports only 64-bit ones, though it’s not fantastic because Linux may add a futex64 or even futex128 at some point. It is far less fine for FreeBSD, where the interfaces support 32-bit keys on all targets but support 64-bit keys only on 64-bit targets (32-bit PowerPC, for example, lacks 64-bit atomics), which means that FreeBSD can’t support the ABI that has been defined to use only 64-bit types on 32-bit platforms. It is annoying on Windows where the native interfaces support all power-of-two sizes from 1-8 bytes but we have to use the indirection layer (which can add false contention) for everything other than 8 bytes.

          Ideally, the library interface would take the size of the type so that the set of types that the host platform supports can be modified in the .so without breaking any code linked against it. Making this change in libc++ is an ABI break.

        2. 1

          That “using” in your example isn’t needed with an enum, but it is with an enum class.