Live data from Hacker News

Show HN: Modifying Clang for a Safer, More Explicit C++

github.com

21–30 of 91 posts

Re: Show HN: Modifying Clang for a Safer, More Explicit C++

#21

> - All basic types (excluding pointers and references) are const by default and may be marked 'mutable' to allow them to be changed after declaration If you're not changing how const works, then this has limited utility in C++ because C++ const has all sorts of problems (e.g. not transitive). Also, what does the "mutable" annotation for a free function (i.e. main) mean? That just seems weird. > - Lambda capture list…

Thank you for this great feedback. I'll do my best to respond to each of your points: WRT const, you're correct and I'd need to go further in updating const behaviors in the language. I stole this idea from Rust (sort of) in that variable declarations in that language are const by default. Essentially, I wanted to 'flip' the semantics in C++ to match, and use mutable to allow variables to change after their declarati…

> [&] is handy indeed yet this was motivated by my experience in legacy heavy codebases where there are often many variables in scope and some with external consequences (e.g. file descriptors, sockets). I don't want these accidentally captured if the lambda invocation site has lifetime implications beyond those resources.

I think your solution to that problem goes the wrong way, though. The problem is whether or not a lambda can survive past the immediate usage, not what it captures. Listing those resources explicitly still gives you the same bug, banning [&] didn't avoid it.

I'd suggest instead an approach where a template taking a callable annotates whether or not it's "inline". If it is inline, then [&] should just be the default even. If it's not, then ban [&]. Possibly ban taking anything by reference if it's not a synchronously-used lambda even.

(inline / non-inline terms here cribbed from Kotlin https://kotlinlang.org/docs/inline-functions.html - probably there's a better word for it, but whatever)

Re: Show HN: Modifying Clang for a Safer, More Explicit C++

#22

I think you might be onto something with regards to the general idea, but most of your particular rules I disagree with. vector for example is very strange; there's no reason vector shouldn't work. With respect to lambda captures always being explicit, it's a far heavier restriction than you (and many) people realize—sometimes you literally cannot know what's inside the lambda to be able to capture it (look up the SC…

Thank you for your thoughtful response. vector wouldn't work because copy semantics wouldn't apply for a constant type, so mutable would be needed (as you rightly pointed out). I'm not sure that vector should work unless the vector container was updated to move its elements by default (another commenter suggested move-by-default rather than copy-by-default as well). I've used RxCpp in the past and know what nightmare awaits should you have to explicitly state lambda captures, yet I've seen too many devs over capture with subtle bugs as a result. Is there a compromise here? I'm not sure that goto is required when one could use do { ... } while(false); with break statements for cases where goto would've been used (not ideal, but again this is an iterative approach). C style casts to void are useful for some memory operations but I'm not sure there's a case where they're required.

If you would, I'd love to hear some of your rules as it's clear you have a lot of C++ experience. Can you send some along? Thanks!

Re: Show HN: Modifying Clang for a Safer, More Explicit C++

#23
post #7

Ok some more suggestions: - Pointers aren't arrays. - no implicit conversions at all. - require fields to be initialized before use/end of constructor

Some implicit conversions are okay, like type promotion from int to double. Some type coercions are fraught, like char to int or back again. I agree that array decay to pointer could be explicit, and pointers shouldn't cast to arrays.

implicit int to double is really, really bad! it can silently truncate - double can only store 53 bits integers so for large integers the result will not be an integer!

in general, lossy conversions should never, ever be implicit

Re: Show HN: Modifying Clang for a Safer, More Explicit C++

#25

Earlier quoted context omitted.

Some implicit conversions are okay, like type promotion from int to double. Some type coercions are fraught, like char to int or back again. I agree that array decay to pointer could be explicit, and pointers shouldn't cast to arrays.

implicit int to double is really, really bad! it can silently truncate - double can only store 53 bits integers so for large integers the result will not be an integer! in general, lossy conversions should never, ever be implicit

Great point, I was thinking of ints as 32 bits. You're absolutely correct for 64 bit ints!

Re: Show HN: Modifying Clang for a Safer, More Explicit C++

#26
IMO it would help adoption if you supply a clang-powered rewriter into and out of your language variant. It allays the fear of losing your codebase if the compiler project dies.

Reverse the default for typename. Currently some_class::thing is assumed to be an expression where 'thing' is a variable, when we don't know which template pattern to use because there may be an explicit specialization on the T that the user chooses. Hence, we have to say "typename std::vector::iterator it;" instead of just saying "std::vector::iterator it;". Instead, reverse that and assume it's a type by default unless shown that it's an expression. You'll need a new keyword for that, replacing "typename".

Remove the promotion-to-int rules. Currently in C (and in C++)

  unsigned short test(unsigned short a, unsigned short b, unsigned short c) {
    unsigned short x = a * b * c;
    return x;
  }
can have UB as signed integer overflow because any math done on an object smaller than int gets promoted to int. (No, you can't fix this with "(((unsigned short)x) * ((unsigned short)y))" the promotion happens on 's LHS and RHS, if those have types smaller than int.) Beyond this, people seem to expect that the type of the variable declaration will appertains to the calculation on the right, but it doesn't. For instance people seem to think "float f = a + b;" can't overflow where 'a' and 'b' are ints, because the assignment is going into a float.

I haven't thought this idea through completely yet. Extend pointer types to include a static allocation identity as part of the type. Address-of local variable or global variable should produce one of these pointers. A "static allocation identity" is a special-typed zero-size variable, so you can stick it in code or as a class member. You could have pointers that were guaranteed to be allocated by THIS allocation point, instead of pointing to every possible T in the program. I'll fake up a syntax, "tree_node ^ tree::node_alloc ". It's known not to alias any other TreeNode the program might have, it has to be attached to the allocation point owned by that specific "node_alloc" in that object. (Let me phrase it differently. A tree in C or C++ has pointers which can point anywhere as long as it's another tree node type. That could be pointing to a different tree, it could be a self-pointer, it could be pointing up the tree, and so on. If your tree_node class has an allocation root, you can say that the pointers are things allocated through this allocation root. They can not outlive the allocation root. They are distinct from the things allocated by other allocation roots, which are the same tree_node types, but different tree_node objects. The node's list of children is std::vector>> so it clearly only holds pointers it allocated itself.)

There's another problem with pointer related to the above. Some code I saw used a "T &get_or_default(Container &c, K key, V &default);" and the problem was that people would call it with a temporary for the default, like "Value &x = get_or_default(mymap, key, Value());" and they'd be holding a dangling reference. If you could make that an error, that'd be great. Maybe we use a trick like the "allocation root" above and treat pointers or references to temporaries have different type from the local variable. Then get_or_default takes and returns a reference-to-temporary and attempting to assign that to a reference in a variable declaration fails. Unlike the previous "allocation root" idea where you indicate the only thing you accept, this would be a case where you accept all allocation roots except one, the "temporaries" allocation root.

As far as I know, no compiler takes advantage of the freedom of the order of operations except in the most trivial ways. Everyone knows that in "f() g() + h()" that * must happen before +, but people think this means that f() and g() must happen before h(). No, they may happen in any order at all. I had to fix a lot of code that did "Print(stream.read(), stream.size())" where "read" updates the pointer and leaves size == 0: gcc ran stream.size() first and clang ran stream.read() first, setting the subsequent size to zero. Similar issue with "expr1() = expr2();" expressions.

Extend switch() and case to work on objects with any operator== defined. Add a statement for fallthrough and default to 'break;' before the start of the next case-label. Give each case label its own scope so I can declare variables in there without adding my own curly-braces. (Bonus 1 can you design a way to ensure that case labels are not overlapping? May require something other than operator==. Bonus 2 can you allow cases to be structured binding matches, similar to Rust?)

Speaking of structured binding, it's great but doesn't allow nesting. This

  std::vector>> v;
  for (auto [name, [lhsid, rhsid]] : v) {
is code I actually wanted to write in the past week yet that's a syntax error.

Add the ability to declare object inheritance ("class Derived : Base;") so that I can cast between them before writing out the body of the derived class. Also allow me to write out the entire class tree with no possibility for extension in another translation unit. The "final" keyword states that a class may not be derived from, but I usually have a Base class which does have subclasses, but a known list of subclasses that will never grow without recompiling the whole project. Currently the compiler has to assume I could write a new subclass and compile it into a shared object that the existing program dlopen's and the existing program will work. It's crazy. No, I have the final tree not just some leaf classes, please devirtualize the whole thing for me.

Are ABI changes on the table? Explicit template instantiations and explicit specializations should mangle differently. See my comment elsewhere: https://github.com/dealii/dealii/issues/3705#issuecomment-11...

If I think of some more, I'll reply to myself.

Re: Show HN: Modifying Clang for a Safer, More Explicit C++

#27
const-by-default is definitely nice. Does this extend to both sides of a pointer type? Does int * refer to int const * const?

There is nothing wrong with [&] for short-lifetime lambdas. Lambdas passed to std algorithms or immediately invoked lambdas come to mind.

edit:

Are data members also const by default? How do I declare a non-const data member that is const when accessed within a const member function? (so non-const non-mutable in original c++)

Re: Show HN: Modifying Clang for a Safer, More Explicit C++

#28

I think you might be onto something with regards to the general idea, but most of your particular rules I disagree with. vector for example is very strange; there's no reason vector shouldn't work. With respect to lambda captures always being explicit, it's a far heavier restriction than you (and many) people realize—sometimes you literally cannot know what's inside the lambda to be able to capture it (look up the SC…

Thank you for your thoughtful response. vector wouldn't work because copy semantics wouldn't apply for a constant type, so mutable would be needed (as you rightly pointed out). I'm not sure that vector should work unless the vector container was updated to move its elements by default (another commenter suggested move-by-default rather than copy-by-default as well). I've used RxCpp in the past and know what nightmare…

> I'm not sure that vector should work

Well, I think it "should" work in the sense that I shouldn't have to type "vector" just to get a vector of mutable ints. It's just too much typing for zero benefit. How exactly you make that work is a separate question; you can do it at both the the language and library level. A compromise might be to make 'mutable' be a storage class (like 'register', or like how it already is for class members) rather than a type qualifier. Note that even making it a storage class has a downside: 'return v;' will now copy-construct its output instead of moving it. You'd have to mess with the const rules to get around that. It might be possible but I'd need to think through the implications and actually play around with it for a while before I could suggest that it would actually work well.

> lambda captures [...] Is there a compromise here?

I don't know honestly. One idea could be to see if some dataflow analysis could tell you if the lambda might leak from the scope it's declared in, and you could warn on that. I think there are already tools (like clang-tidy, cppcheck, etc.) that give you warnings of this sort; I'm not sure if they fully handle this case though, you'll have to check and see if those handle the cases you want. It almost certainly won't be something you could whip up in a few hours, in case that's what you were hoping for.

> I'm not sure that goto is required when one could use do { ... } while(false); with break statements for cases where goto would've been used (not ideal, but again this is an iterative approach).

That's in no way a substitute for a goto. Sometimes you really do need the ability to jump in, not just jump out. Imagine a state machine/coroutine/etc.—it's not impossible to write them without goto, but sometimes you'd have to go through contortions and write unnatural/unmaintainable logic to write them without goto. Yes C++20 has coroutine support now but it's mediocre at best and isn't suitable for every use case.

> C style casts to void are useful for some memory operations but I'm not sure there's a case where they're required.

Edit: (void) isn't required anywhere I know of, but there are lots of places where it's helpful to have, and completely unhelpful not to have. Here's one:

  void foo(void *p)
  {
  #if NDBUG
    bar(p);
  #else
    (void)p;  // suppress "unused parameter" warning
  #endif
  }
Sure you can do static_cast(p) but that's not buying you anything. It's not the end of the world, but it's just wasting your time and making your code more verbose to read. I don't have a problem with more verbose typing when it actually buys you something, but there are cases where it doesn't, and this is one of them.

In fact, a better rule might #5 below. I'm not sure there's a reason to ban the C-style cast entirely; it could be much more useful and safer than it is now.

Meta-rule of thumb: you need to make sure you're familiar with the vast array of use cases and scenarios people encounter in real-world C++ before you can come up with rules for other C++ devs to follow. The committee itself has a hard enough time doing this for a good reason—because it's hard! If you are going to propose that some feature is unnecessary, it should be a conclusion you draw after you've already used that feature in its "most useful" context (and found a good alternative)—not before that. Most features have some very compelling use cases, so if you haven't found a compelling use case for a feature ("compelling" assuming you disregard any downsides it might have in other contexts) then there's a good chance you simply haven't come across it yet, rather than it having been unnecessary to begin with. It's usually enlightening (and honestly kind of fun) to try to figure that out before rushing to get rid of it.

> I'd love to hear some of your rules

Sorry I wrote this comment but forgot to respond to this part. I'd have to sit down and think through a lot of them before I can share them with any confidence honestly. But just going off the top of my head, here might be a few:

(1) Conversion operators (like constructors as you mentioned) should probably be explicit by default too

(2) Shadowing local variables (or parameters) in a surrounding scope should probably require something like [[shadow]] somewhere to make it abundantly obvious it's intentional (and its use cases would be incredibly rare)

(3) Initializing a variable by passing itself as an argument should be disallowed (so struct MyClass { int x; MyClass() : x(x) { } }; should be illegal, i.e. the equivalent of -Werror=init-self should be mandatory)

(4) value-initialization should probably be the default, but with a way to override it and perform default-initialization when there's actually a reason to (but perhaps -Wuninitialized should still treat the variable as uninitialized regardless)

(5) Perhaps the C-style cast should really be equivalent to a static_cast except in cases where a dynamic_cast/reinterpret_cast/const_cast would also be legal, in which case it should be an error? That would make it safer than static_cast (since it's more restrictive in where it's allowed), rather than more dangerous, and it would require less typing as well.

Re: Show HN: Modifying Clang for a Safer, More Explicit C++

#29
post #24

what's the motivation for removing `goto`, is this something that you find being abused? I code in c++ for work, and I almost never see anyone using it without a good reason.

My personal opinion is that a programming language should instead of 'goto', have explicit constructs for those things that 'goto' is most often used to emulate:

• Breaking out of nested loops

• Clause after loop that has run to its end-condition without a break, return or throw. Python allows an 'else'-clause after a loop, but IMHO "default" would be a better keyword.

• Error handling (C++ has exception handling already, but there are alternatives)

Re: Show HN: Modifying Clang for a Safer, More Explicit C++

#30
post #8

Rather than build a new compiler, I wonder if this might be easier to integrate as a static checker. IMO clang static checks are not that difficult to write. The hardest thing can be the query to find the interesting elements. But you're banning/requiring fairly high-level language elements so they should be pretty easy queries to write.

Agreed, that's why I started by modifying clang. I think we can start dropping some of the crufty legacy in the C++ language without throwing it all out and starting again. While clang tidy could be used to check for a lot of these, I wanted to show that we could change the language directly and what that could look like.

> I think we can start dropping some of the crufty legacy in the C++ language (...)

Do you have any concrete example of what you perceive as being "crufty legacy"?

Post reply on HN