C++ needs a linter that rejects code that doesn't meet a certain standard ala Rubocop.
C++ needs a linter that rejects code that doesn't meet a certain standard ala Rubocop.
EDIT: In particular, the clang tidy linter has a way of checking your code against the C++ core guidelines [2], which is a set of modern best practices for the language.
I've never met a linter I liked more than a real compiler, not even close. Linters for scripting languages only exist because the scripting languages aren't compiled, and they are only lightweight second rate optional type checkers. Linters' attraction and usefulness is that you aren't required to use them, so you can iterate quickly and run code without having to wait for the compiler every single time you save your file. Linters are not better at keeping you within good style and safety bounds, generally speaking.
Yes, a bare minimum standard, which must be backwards compatible. What a compiler enforces is not best practice, it is bare minimum. Something enforcing best practice has to be allowed to be backwards incompatible, since newer better ways of doing things will always come up (that's how languages evolve), and you want to discourage the older stuff.
"I've never met a linter I liked more than a real compiler" -- it's not an either or situation, use a linter on top of the compiler.
-Wall gives you a lot more than bare minimum. But, you can't really call it bare minimum when you compare compilers to linters in general. If a compiler enforces bare minimum, then a linter enforces less than bare minimum. Linters in general do not detect type errors that compilers catch and that prevent the program from running. A compiler is a bigger, better safety net than a linter.
I have ~25 years experience with many linters and many compilers, and my opinion is that compilers provide more security than linters, on the whole. When I can, I try to use a real compiler for production use of scripting languages, for example Google's closure compiler in JavaScript rather than linters like eslint or jslint.
> it's not an either or situation, use a linter on top of the compiler.
This is true, and I agree completely, it's not an either-or situation. That's in part what I was trying to say. The compiler already does linting and it does much better static analysis, so you don't need to choose, you can just use a compiler. There are certainly higher level style analyzers, clang-tidy was mentioned already, and there are others.
PEP-8, and Rubocop are in part a set of guidelines designed to prevent code mistakes and type errors precisely because the languages they represent don't come with a static type-checking compiler. Many of the linting rules and best practices only exist to prevent unsafe code that would have been prevented if there was a compiler involved, and only need preventing via style guides and linters because a compiler is not necessarily there.
There are certainly best social practices that some linters enforce that many compilers don't, but for that it doesn't make any sense to avoid learning C++. The OP's comment - that he feels like C++ is a minefield without a linter - is missing the forest. It is easier to know you're writing correct C++ than it is to know you're doing correct Python, because of C++'s compiler. Unfortunately no compilers or linters are that good at preventing you from writing code that runs and then crashes.
I'm considering linters as something you use in addition to compilers. Yes, they enforce fewer checks, but the assumption when running a linter is that the code compiles, already. Or, at least, that's what the assumption should be.
> This is true, and I agree completely, it's not an either-or situation.
Ah, okay. Yeah my issue was mostly with the either-or framing here.
> because the languages they represent don't come with a static type-checking compiler.
Yes, but this thread is about C++, which is a different world.
-Wcast-align -Wcast-qual -Wctor-dtor-privacy -Wextra -Wformat=2 -Wimplicit-atomic-properties -Wmissing-declarations -Wno-long-long -Wnon-virtual-dtor -Woverlength-strings -Woverloaded-virtual -Wredundant-decls -Wreorder -Wswitch-default -Wunused-variable -Wwrite-strings -Wno-variadic-macros -Wno-unused-local-typedef -Wno-language-extension-token -fstrict-aliasing
and optionally: -Wconversion -Wdocumentation -Wfloat-equal -Wmissing-prototypes -Wold-style-cast -Wunreachable-code
and this is still insufficient. Why? Because this is only checking syntactical and semantic correctness. It can't check for violation of local policy, such as:no use of new/delete (use make_unique or make_shared)
correct and consistent use of virtual inheritance across the entire codebase
rule of three and rule of five usage
etc.
These are mostly higher-level concerns. Stuff which other people on my team might introduce, and/or miss in review.
I don't yet have anything automated in place for this, but clang-tidy is on my todo list for investigation.
Have you used any linters? Because linters are even worse than what you're describing with the C++ compiler. What they do is complain about style issues that some people believe are likely to lead to syntactical and semantic errors, without actually checking for the syntactical and semantic errors like a compiler does. Linters also don't check for violation of local policy, unless you extend them yourself. (And feel free to extend clang's warnings to suit your needs... it is open source.)
I fully 100% completely agree that C++ tooling could be way better, and I wish it was too. But that's not the issue we're talking about here at all.
The same that micro-optimize every single line of code they write without ever running the code through a profiler.
Matrix multiplication is super simple. Strassen is more complicated. Coppersmith–Winograd is brain bending (besides being slower on actual data and hardware).
The rule of thumb is: you have a complexity budget and a performance budget. Know what they should be, and choose economically.
If you mean the warnings, it's because they're way over the top.
switch(enabled)
> warning: switch on bool
switch(static_cast<int>(enabled))
Sigh.Now I know, "why would you ever switch on a bool?! It's only two possible states!"
Because it was in an ARM CPU emulator that decoded three to five fields per instruction, most of which were multiple bits long, but a few were just one bit. It gave a structured, consistent look to the code for each field to be implemented with a switch.
That's just one example, though. My current list of, "are you kidding me?" warning disables for Clang are:
#pragma clang diagnostic ignored "-Wunknown-pragmas"
#pragma clang diagnostic ignored "-Wempty-body"
#pragma clang diagnostic ignored "-Wparentheses"
#pragma clang diagnostic ignored "-Wreturn-type"
#pragma clang diagnostic ignored "-Wswitch"
#pragma clang diagnostic ignored "-Wswitch-bool"
#pragma clang diagnostic ignored "-Wtautological-compare"
#pragma clang diagnostic ignored "-Wabsolute-value"
This was all for perfectly legal, unambiguous code. Anything even remotely suspicious I'd fix. You probably still think I'm a macho coder anyway, but I'm honestly not. I'm just rather obsessive about consistency and simplicity in my code.I would really not enjoy working on a codebase that forced me to use -Wall -Wextra -Werr.
>= 0 on an unsigned value is the classic one.
Another time I've used it is just pure aesthetics:
if(addr >= 0x0000 && addr <= 0x7fff) return ram.read(addr);
if(addr >= 0x8000 && addr <= 0xbfff) return cpu.read(addr);
if(addr >= 0xc000 && addr <= 0xffff) return apu.read(addr);
Looks nicer than: if(addr <= 0x7fff) return ram.read(addr);
if(addr >= 0x8000 && addr <= 0xbfff) return cpu.read(addr);
if(addr >= 0xc000 && addr <= 0xffff) return apu.read(addr);
And the compiler will delete the unnecessary comparison anyway, so there's no great reason to warn about it.And now imagine you want to make it even nicer, so you add between(addr, lo, hi) as a macro: (note: not a real example -- I wouldn't do this for this case, just for demonstrative purposes.)
if(between(addr, 0x0000, 0x7fff)) ...;
if(between(addr, 0x8000, 0xbfff)) ...;
if(between(addr, 0xc000, 0xffff)) ...;
Now you have the generic code triggering the warnings again.Every warning case I've described can result in bugs, but the problem is when you intended to do what you did, you can't get the compiler to shut up about it there.
For the warnings I disable, they're things I do all the time. I can't recall ever typing = when I meant ==; whereas I combine assign + test into a single statement very frequently.
The warnings I keep on are the ones where I'd never want to do those things, and it'd never be proper code. Those are great warnings to have in place.
weird_non_trivial_copy_constructor_type foo() {
switch(value & 3) {
case 0: return a;
case 1: return b;
case 2: return c;
case 3: return d;
}
}
You can fix it by adding a default:, which will generate worse code; or by adding a dummy return (usually easy, but not always) after the switch; or by using compiler-specific intrinsics like __atribute__((unreachable)). Or you can turn off the warning.If they add [[unreachable]] to the standard, then I'll consider switching to that and re-enabling this warning. Because I'll admit, I have made the mistake of omitting a return type before.
For gcc and clang I've also had some success with the diagnostic pragmas, which I stick in pshpackN.h-/poppack.h-style header files. For example, I use these to stop both moaning about flexible array members in C++ code.
Push header:
#ifdef __GNUC__
#pragma GCC diagnostic push
#ifdef __clang__
#pragma GCC diagnostic ignored "-Wc99-extensions"
#else
#pragma GCC diagnostic ignored "-Wpedantic"
#endif
#endif
Pop header: #ifdef __GNUC__
#pragma GCC diagnostic pop
#endif