Hacker News new | past | comments | ask | show | jobs | submit login

So the f-string literal produces a basic_formatted_string, which is basically a reified argument list for std::format, instead of a basic_string. This allows eg. println to be overloaded to operate on basic_formatted_string without allocating an intermediate string

  std::println("Center is: {}", getCenter());
  std::println(f"Center is: {getCenter()}");  // same thing, no basic_string allocated
In exchange we have the following problems

  // f-strings have unexpected type when using auto or type deduction.
  // basic_string is expected here, but we get basic_formatted_string.
  // This is especially bad because basic_formatted_string can contain
  // dangling references.
  auto s = f"Center is: {getCenter()}";

  // f-strings won't work in places where providing a string currently
  // works by using implicit conversion. For example, filesystem methods
  // take paths. Providing a string is okay, since it will be implicitly
  // converted to a path, but an f-string would require two implicit
  // conversions, first to a string, then to path.
  std::filesystem::exists(f"file{n}.dat");  // error, no matching overload
There are two other proposals to fix these problems.





> There are two other proposals to fix these problems.

Most new features of C++ are introduced to fix problems created by previously new features added to C++.


This is becoming such a tiresome opinion. How are concepts fixing a problem created by previous features to the langue? What about ranges? Auto? Move semantics? Coroutines? Constexpr? Consteval? It is time for this narrative to stop.

Move semantics is only needed because C++ introduced implicit copies (copy constructor) and they of course fucked it up my making them non-destructive so they aren't even 'zero cost'.

Constexpr and consteval are hacks that 1) should have just been the default, and 2) shouldn't even be on the function definition, it should instead have been a keyword on the usage site: (and just use const)

  int f() { ... } // any old regular function
  const int x = f(); // this is always get evaluated at compile time, (or if it can't, then fail to compile)
  int y = f(); // this is evaulated at runtime
That would be the sane way to do compile time functions.

I agree that I would have preferred destructive moves, but move semantics makes C++ a much richer and better language. I kinda think pre-move semantics, C++ didn't quite make "sense" as a systems programming language. Move semantics really tied the room together.

    const int x = f(); // this is always get evaluated at compile time, (or if it can't, then fail to compile)
That's very silly. You're saying this should fail to compile?

    void foo(int x) {
        const int y = bar(x);
    }
There's no way the compiler can run that, because it doesn't know what x is (indeed, it would have a different value every time you run the function with a new argument). So your proposal would ditch const completely except in the constexpr case, everything runtime would have to be mutable.

So you respond "well, I didn't mean THAT kind of const, you should have a different word for compile-time constants and run-time non-mutability!" Congratulations, you just invented constexpr.

There are many bad things about C++, but constexpr ain't one of them.


>There's no way the compiler can run that, because it doesn't know what x is (indeed, it would have a different value every time you run the function with a new argument). So your proposal would ditch const completely except in the constexpr case, everything runtime would have to be mutable.

Yeah, I see no problem with that. Non-constant expressions usage of 'const' has always just seemed like a waste of time for me, never found it useful. But I guess a lot of people really liking typing const and "preventing themselves from accidentally mutating a variable" (when has that ever happened?), so as a compromise I guess you can have a new keyword to force constant expressions:

  constexpr auto x = foo(); // always eval at compile time
  const auto x = foo(); // old timey const, probably runtime but maybe got constant folded.
but it's not really a big deal what they keyword is, the main point was that "give me a constant value" should be at the usage site, not at the function definition.

> the main point was that "give me a constant value" should be at the usage site, not at the function definition.

The issue is, not everything can be done at compile time, and so “I can use this at compile time” becomes part of the signature because you want to ensure that it will continue to be able to be used that way. Without it, changes in your function could easily break your callers.


Exactly right. There's a huge benefit to encode the ability for compile-time evaluation in the signature of the function itself. Much better than doing it "ad-hoc", like how template instantiation does it. Sometimes it will work, sometimes it doesn't. constexpr functions always work.

I like const because I can look at a function signature and know that nothing downstream is mutating a parameter. I can also write a function that returns a const reference to a member and know that nobody in the future will ever break my invariants.

This isn't about "oops, I didn't mean to mutate that." This is about rapidly being able to reason about the correctness of some code that is leveraging const-qualified code.


> "preventing themselves from accidentally mutating a variable" (when has that ever happened?)

I can't count the number of times I've seen someone new to the language use map::operator[] without realizing that it's a mutating operation.


It’s extra delightful that vector::operator[] isn’t mutating, so you can change a vector to a map and get rather different semantics.

map::operator[] should just be [[deprecated]] - even if you want to mutate the map it's probably not the best way to do what you want.

I kinda like it on occasion. Works like pythons defaultdict. Like, if you wanna count something:

    for (const auto &thing: collection) {
        counts[thing]++;
    }
Works nicely, you don't have to check if it's already there before ++ing it. As long as you know that's what operator[] does, it comes in handy more than I would've expected.

Yeah. It has its uses. You could accomplish the same with the rather verbose `counts.try_emplace(thing).first->second++` but nobody wants to type that (even if it is more explicit about what it's doing).

Another popular use case is something along the lines of:

    std::unique_ptr<T>& ptr = ptrs[key];
    if (ptr != nullptr) {
      ptr = std::make_unique<T>(...);
    }
That said, I don't know what behavior I'd want if maps didn't automatically insert an element when the key was absent. UB (as with vector)? Throw an exception? Report the incident to Stroustrop? All these options feel differently bad.

> when has that ever happened?

Maybe it's not a concern in C-family languages, but rust's culture of defaulting to let and only using mut when it's specifically required does feel very pleasant and ergonomic when I'm in that headspace.


> (when has that ever happened?)

to me, pretty much a few times per week at least


Could have been if backwards compatibility was not a thing indeed.

Move constructors are not needed, they don't solve a 'problem', but improve on previous semantics.


Eh not really accurate because C's const means immutable not actually constant. So I get introducing constexpr to actually mean constant. But, yeah, constexpr x = f() should probably have worked as you described.

const is different in C++ from const in C. const variables in C++ are proper compile-time constants. In C they are not (the nearest equivalents are #define and enum values).

So in C++ "const x = EXPR" would make sense to request compile-time evaluation, but in C it wouldn't.


They absolutely are not. Look at this range for-loop:

    for (const auto item: vec) { ... }
`item` is not a compile-time constant. It's different every run of the loop.

Ouch, but thanks. I learned something today - something I'd long forgotten. I like your example, it shows the point well. (Though, there are circumstances when a compiler can unroll such a loop and infer a compile-time constant, it wouldn't qualify as a constant expression at the language level.)

It's been so long since I used C++ for serious work that we weren't using C++11, so neither auto nor range-for were available. It would be uncommon to see "const type = " with a non-reference type and a non-constant initialiser.

Even with your example, some styles avoid "const auto item", using either "auto item" or "const auto& item" instead, because the "const" matters when taking a reference, not so much with a copy.

But I appreciate your point applies to const variables with non-constant initialisers in general, in the language.

There was once a big deal in literature about const in C++ being the "better" alternative to how #define is commonly used with C for constant values, and it seemed applicable to the thread as a key distinction between C and C++, which the parent commenter seemed to have conflated by mistake.

But I'd forgotten about const (non-reference) variables accepting non-constant initialisers, and as I hadn't used C++ seriously in a while, and the language is always changing, I checked in with a couple of C++ tutorials before writing. Unfortunately those tutorials were misleading or too simple, as both tutoruals said nothing about "const type x = " (non-reference/pointer) being uwed in any other way than for defining compile-time constants.

It's bit embarrssing, as I read other parts of the C++ standard quite often despite not using it much these days. (I'm into compiler guts, atomics, memory models, code analysis, portability issues, etc.). Yet I had forgotten this part of the language.

So, thanks for sending me down a learning & reminder rabbit-hole and correcting my error :-)


I thought the whole point of ranges is to solve problems created by iterators, move semantics to take care of scenarios where nrvo doesn't apply, constexpr and auto because we were hacking around it with macros (if you can even call it that)?

Iteratively improving in previously released features does not imply fixing issues caused by those features.

Constexpr and auto have nothing to do with macros.


To me, redoing things that are not orthogonal implies that the older version is being fixed. Being fixed implies that it was incorrect. And to clarify, sure, auto types and constexpr are entirely new things we didn't have (auto changed meaning but yeah), but we were trying to "get something like that" using macros.

> To me, redoing things that are not orthogonal implies that the older version is being fixed

The older version is being improved, especially for ergonomics. Regarding your examples, ranges do not obsolete iterators, they are just a convenient way to pass around iterator pairs, but actual range are better implemented in terms of iterators when they are not just a composition of ranges. Similarly move semantics has little to do with nrvo (and in fact using move often is suboptimal as it inhibits nrvo).

Again, I have no idea how constexpr and auto have anything to do with macros.


> How are concepts fixing a problem created by previous features to the langue?

Concepts fix implicit template requirements

> What about ranges?

Fix the bad iterators

> Auto

Fix the the overly long type names

> Move semantics

This is mostly necessary because of excessive copies that cpp does

> Consteval

Fix cases where constexpr couldn't do it at comptime


It might be getting tiresome because it keeps being true, so people keep pointing it out.

Honestly can't tell if this is sarcasm. XD

auto is fixing the problem of long-ass type names for intermediaries thanks to templates and iterators.

Move is fixing the problem of unnecessary mass-construction when you pass around containers.

std::ranges was introduced because dear fucking god the syntax for iterating over a partial container. (And the endless off-by-one errors)

concepts, among other things, fix (sorta) the utter shit show that templates brought to error messages, as well as debacles like SFINAE and std::enable_if.

You're right. They're not fixing problems created by previous features. They're all fixing problems created or made massively worse by templates.


Hah. What's interesting about this is that since it doesn't require everything to actually be converted to a string, one can implement things other than just printing. So you could also implement interpretation, eg:

  pylist = python(f"[ y*{coef} for y in {pylist} if y > {threshold}]")

It also allow for things that will set off spidey senses in programmers everywhere despite theoretically being completely safe assuming mydb::sql() handles escaping in the format string:

   cursor = mydb::sql(f"UPDATE user SET password={password} WHERE user.id={userid}")

Yeah. You really want "mydb::sql" to not take a basic_string, only a basic_formatted_string, so it will not compile if the conversion actually happened somehow.

Yes. The basic idea is that there's a specifier that allows a formatted string to transparently decay into an ordinary string (à la array-to-pointer decay) so that "auto" doesn't produce dangling references, and so that chains of more than one implicit conversion can take place.

This seems pretty similar to Rust's `format_args!` macro, which however avoids these issues by being much more verbose and thus something people are less likely to use like in those examples. It does however have issues due to the abundant use of temporaries, which makes it hard to use when not immediately passed to a function. I wonder if C++'s fstrings have the same issue.

One of the two other proposals is user defined type decay, which lets you choose what type auto will be deduced as. i.e. "auto x = y", x might not have the type of y, instead it can be anything you choose…

This is like implicit type conversion on steroids. And all this because C++ lacks the basic safety features to avoid dangling pointers.

Stop using C++ already!


> lacks the basic safety features to avoid dangling pointers

It doesn't. Unfortunately, C++ programmers choose not to use basic safety features for performance reasons (or aesthetics, or disagreement with the idea that a language should take into account that a programmer might make a mistake, but at least performance is a good one), but C++ actually has quite a few tricks to prevent the memory management issues that cause C/C++ bugs.

Using modern C++ safety features won't completely prevent bugs and memory issues, just like using Rust won't, but the mess that causes the worst bugs is the result of a choice, not the language itself.


Tell that to the designers of the C++ standard library, and the new features being added. They're the ones that keep adding new features that depend on references and pointers instead of std::shared_ptr or std::unique_ptr.

I think one problem here is that a lot of codebases have their own smart pointers and unfortunately the only currency type is the unsafe one :(

I don't think this is the only reason. If it were, they could easily have added overloads that work with both std smart pointers and with plain pointers for compatibility. Or they could add pointer type template parameters, maybe with concepts for the right ownership semantics.

That would be too complex. I don't think C++ developers would be able to handle it

shared_ptr and unique_ptr aren’t useful for reasoning about the lifetimes of stack-based objects (unless you’re willing to require that such objects always be dynamically allocated, which is often not a reasonable requirement).

Do these “modern C++ safety features” actually exist in any usable manner?

string_view, for example, is very modern and is utterly and completely unsafe.


> the mess that causes the worst bugs is the result of a choice, not the language itself.

Even the creator of the language admitted that "just write better code bro" approach doesn't work.


Has he? He at least used to be the biggest proponent of it, "just follow these standards and development practices that I had to meticulously develop for the US military, that no tool can automatically check, and you'll be fine!".

Yes, it writes in the slide 6 of presentation that Bjarne himself created: https://github.com/CppCon/CppCon2023/blob/main/Presentations...

Smart pointers were added to the language 14 years ago. You're free to use old C++ with raw pointers and manual memory management, risking dangling pointers, or use modern C++, which provides smart pointers to avoid those issues.

And yet most if not all of the standard library keeps using pointer or reference arguments, not the new smart pointers that would actually document the ownership semantics.

Most arguments to standard library calls don't need to take ownership over memory, using a raw pointer or (const) reference is correct. Generally - smart pointers to designate ownership, raw pointers to "borrow".

If a function takes a raw pointer, you need to check the docs to know if it is taking ownership or not. There is no general rule that applies to the whole of std that functions taking raw pointers assume that they are borrowing the value.

And even if you could assume that pointer parameters represent borrowing, they are definitely not guaranteed to represent scoped borrowing: the function could store them somewhere, and then you end up with other issues. So shared_ptr is the only solution if you care about safety to represent a borrowed pointer. And of that's too costly, but the std designers did care about safety, they could have introduced a std::borrowed_ptr<T> that is just a wrapper around T* but that is used uniformly in all std functions that borrow a pointer and guarantee not to store it.


What are the examples to std functions that uses a raw pointer but does not borrow/expects pointer to be valid past the function call?

The fact that you have to ask that question makes GP's point. There's no way to tell that from looking at a function's interface at present.

Why? If all standard functions that take no ownership/keep references are using raw pointers then it behaves same as user code/C++ devs expect: if a function is taking a pointer then it claims no ownership. You take a look at standard_function(T*) and see raw pointer and then can assume it is not taking ownership or keeping references

The fact that you don't have an answer make's GP's point. This isn't actually a problem with the standard library because the semantics are clear.

I would not say stop using it. But just stick to the really needed features, and stop adding more features every 3 years. Nobody can keep up, not the developers, not the compilers... is just insane.

First we need to rewrite the likes of LLVM, GCC, V8, CUDA,... into something else.

Which is not going to happen in our lifetime, even the mighty Rust depends on LLVM for its reference implementation.


Stop producing a new C++ code as much as possible, then. It doesn't help to nitpick the weakest possible interpretation without acknowledging as such.

Which by definition means no more improvements to LLVM then, as one example, and by consequence no improvements on Rust backend.

"as much as possible" nobody said anything about entirely stopping.

C++ desperately needs a solution for wrapper types that should eagerly decay into the wrapped type unless you pass it somewhere that explicitly wants the wrapper type.

"operator auto" has been proposed a few times. But nailing down the semantics has proven elusive.



Consider applying for YC's Spring batch! Applications are open till Feb 11.

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

Search: