Near-duplicate features of C++ (2017)
nayuki.io
nayuki.io
> int foo(void)/int foo()
In c, int foo() means that foo takes any number of args; c++ rightfully realised that that was stupid and removed that feature, which means int foo() can now be used to mean foo takes no args. Also, int foo(void) is illegal in c++.
Varargs in c are a complete pita to use, not to mention they literally do something different from function overloading/default args.
Constexpr is much nicer and safer to use than a macro.
Using malloc and calloc to make class instances means you can have unconstructed objects, which is a Bad Thing, and while introducing a new operator to do that is not necessarily the best way to handle that, it's a perfectly valid way of doing so.
Libc random is just garbage, and libc++ random is actually decent.
setjmp/longjmp are more complicated to use than try/catch, and don't have all the same functionality.
<iostream> functions are type aware (although really they should have used fmt).
I'm no c++ apologist (it's an awful language), but this listicle is just...not great.
Uh, citation needed. It would make compatibility with C libraries harder for no good reason. I believe it is valid as both declaration and definition.
Both g++ and clang++ accept it without even a warning.
foo(void) seems less likely to cause confusion to anyone who hasn't got that C vs C++ distinction uppermost in their mind - I didn't. Nobody knows C++, we all know a subset of it and a smaller subset than we think we do and actually use, despite pretty constant study! Anything you do that makes a reader less likely to be confused, to make it less likely that they will or should break their flow by looking up the rule, is a good thing (tm).
C++ suffers from "macho coding" styles and ethos quite badly. "I /believe/ I understand this subtlety at this moment so anyone who doesn't is stupid." Even when we didn't understand it ourselves just last week we forget it(!) We fool ourselves into thinking anyone who hasn't got at least a decade of hacking C++ every day is unworthy of reading our code, possibly pretending that 6 months can attain that level of oneness with changing standards.
I try to use popular and safer features without thinking about it and if I want to use something slightly less popular when there is a choice, I ask "what problem am I solving? Is lowering readability a price I'm happy to pay here?" Before charging ahead with it. I probably classify some of the latter as the former due to over-familiarity.
The ideal C++ code is rapidly readable and understandable without extremely taxing effort by a smart highschool student with 2-3 months of C++ experience. (Note the use of the word ideal in that sentence). s/C++/any language/ is also applicable.
Other than following the style of the project you're working on is there any reason to use a reference ever over say a const pointer? Or a const pointer to a const object? The syntax then makes it clear you're not dealing with a pass-by-value copy. Maybe in another world everything would be const by default and have to be marked mutable then references might go away - but C++ couldn't do that and compile C which explains /some/ of the tension inside the language between various C++ features. Shame they didn't make references const by default if they had to introduce them. And member functions defaulting to const and explicit constructors by default. Const construction by default etc. There was an opportunity lost a long time ago to tighten up everything that isn't legal C as far as possible with the programmer having the option with an additional keyword.
if(condition)
I_am_tough_so_I_do_not_brace();
The un-braced block is a classic "macho" pose in code. leaving out a '{' and a '}', two characters, has no benefit in any way, shape or form. It is decrying "I will never make a mistake with this loose razor blade if you do maintaining it is because you suck" There is literally no reason to do it ever, under any circumstances. Yet I have, and we do. We can change. Now watch the arguments presented when you bring un-braced blocks up. They're insanely ridiculous. (vertical space, need static analysis, etc.) If you were betting your retirement savings on no bug in the line of code now or in the future, you'd be very nervous about it sure. And then you would brace it. Unbraced blocks are a minor thing that mostly don't matter. I wouldn't fight too hard over it. It's just that it's so very, very clear that one way is bugs for zero benefit while the other isn't. Meh.So here's Bjarne being "Uber-Macho" to the delight of the conference crowd. Note the quote:
"I think I understand what you're saying and I think you happen to be wrong." (crowd whoops in glee). https://youtu.be/OB-bdWKwXsU?t=4141
This is a pretty ugly way of saying "I disagree and this is why..." But the crowd loves it. That attitude gets into our code. "Macho coding" is the best descriptive name I can manage. Maybe there's a better one? (Note I agree with the content of Bjarne says, I just think he's being an asshole about it, why do that? https://www.urbandictionary.com/define.php?term=Waltersobcha... )
One reason is to use overloaded operators. With pointers, adding two matrices which are arguments to a function would look like `( <star>a ) + ( <star>b )` (using asterisk makes the text italic); with references it's just `a+b`.
Another, stylistic, reason for which I use references is as a hint for two things: 1) the parameter is NOT optional (whenever you see a pointer parameter you wonder whether it can be null), and 2) that no ownership is transferred.
For these reasons, unless I actually need a pointer (e.g. optional parameter), I use references: less syntactic clutter at both call-site and use-site. With a decent IDE, it's easy to see how the parameter is passed.
> And member functions defaulting to const
That wouldn't fly and in many scenarios it wouldn't make sense because whether an operation is "const" is unclear. Like, you have a class representing a socket, and methods like read, write and close. None of the methods change the in-memory contents of the class (so, yes they're technically const), yet their behavior changes depending on the sequence in which they're called, e.g., calling close before write instead of the other way around (so no, they're not really const).
If "everything should be const unless it has to be mutable" is a reasonable position to take - which many seem to, then it should be the default, right? Too late for that now.
(<star>a) + (<star>b) yeah it's reasonable use. I can't say I care much. Obviously need references for the return value of an operator overload too if you're having operator overloading. It's reasonable. Doesn't thrill me, people have different taste on it which is reasonable too..
IMHO if the answer is "IDE" that's a workaround to a language issue and it may not be a big language issue either but it exists. C++ and Java both have some of that. Is the function taking the arg by mutable reference because Dave forgot? Or was did he have constication hell and a deadline so applied prune juice liberally? Or is it an output? Documentation in the header, popped up by your ide/text editor? sure, but that's also a workaround for something that would be nice if it were clear just by looking.
class foo {
void b1();
void b2() const;
void b3() mutable;
}
b1, b2, and b3 are all distinct.For a language like C++ I don't think that's a reasonable position. "Good" class design is about a class encapsulating some behavior and providing methods to access that behavior. Any "interesting" behavior is based on mutable state, whereas const methods are useful only as observers of the state. So "const" as default would make the code more cluttered...
Heck, Java and C# don't even have const (though recent version of C# have added kind-of struct constness...)
> sure, but that's also a workaround for something that would be nice if it were clear just by looking.
In any sufficiently large program, nothing is clear just by looking. Such "local artifacts@ (like, explicit address-of operator) are irrelevant, the fewer the better. Writing `unique_lock<mutex> lk(&my_mutex)` would not add the tiniest bit of clarity compared to what you write now `unique_lock<mutex> lk(my_mutex)`
> Is the function taking the arg by mutable reference because Dave forgot?
That's easy to check: change it to const and see whether the method still compiles.
> Documentation in the header, popped up by your ide/text editor?
In the end, you always have to go to the function's definition and inspect it, and possibly follow the call chain further to find out what's really happening (i.e., the top-level call site is no longer relevant), and that's where IDEs shine. And no amount of syntax changes is going to save you from a const reference being aliased by a non-const one.
"Everything should be made const in your code unless it can't be" is advice metered out by the likes of John Carmack, Scott Myers etc. so it's pretty popular and oft repeated. The prevailing C++ zeitgeist perhaps. Could still be wrong, absolutely, don't stop thinking there by any means. Constification of a codebase can be a major pain in the butt.
Yes, definitely. It codifies nullability. const pointers should _typically_ be const references instead, see https://google.github.io/styleguide/cppguide.html#Reference_...
> Maybe in another world everything would be const by default and have to be marked mutable then references might go away
I'd definitely +1 a references are const by default, but that would not make references go away at all. You'd still have references (non-null) and pointers (null) just for the reason of non-null vs. null. This is a very useful distinction. So much so Kotlin built an entire language around it.
> The syntax then makes it clear you're not dealing with a pass-by-value copy.
Which is not a useful behavior difference. If it's const why do you care if it's pass-by-value or pass-by-reference? It doesn't change behavior and it doesn't even tell you anything about the performance of the call (there are plenty of pass-by-pointer calls that are slow and plenty of pass-by-value that are not - it depends on the size of the argument, and what the function is doing)
foo(bar, baz, bajinga);
Which of those are pass by value, by reference or by const reference? I say it would be useful to know that by inspection from just reading that line of code. You don't have to agree. And you care about by value vs by reference because copy constructors exist and you need to reason about performance or if you don't need to why are you using c++ anyway?You mostly care whether it's by value or by reference because what is passed could be altered with the latter unless it's marked const. But which is it? Stop! Stop what you're reading right now and go and look at the foo() signature. 9 times in 10 you won't be surprised, the arguments should not be altered by the function call but you might want to check that, then get back to whatever you were actually trying to understand with a break in the flow of your understanding and the prospect of some context fading slightly from your short term memory.
But yeah some people are so smart that there is literally nothing in coding readability they need and sneer at those of us who take a different approach. Such people never make mistakes. Twas ever thus, well before C++.
Your example in Rust would read like this:
// Definition
fn foo(a: u8, b: &u8, c: &mut u8) -> i32 {
a = 0; // Local change only
println!("{}", *b); // Read pointee
*c = 1; // Write pointee
return 2;
}
// Call site
let bar: u8 = 3;
let baz: u8 = 4;
let mut bajinga: u8 = 5;
foo(bar, &baz, &mut bajinga);That's an implementation detail of the function you're calling, not a performance issue at the call site. The call site should not have an opinion on this. Either the object is big enough it should be pass-by-reference, or it's not. You don't need to sprinkle that detail at every call site.
> You mostly care whether it's by value or by reference because what is passed could be altered with the latter unless it's marked const.
For mutable parameters I agree, those should be passed by pointer. Because they actually have an influence at the call site.
But const references, which is what we were talking about, have no behavior change at the call site. So they should not stand out or be marked special, since they aren't.
> But yeah some people are so smart that there is literally nothing in coding readability they need and sneer at those of us who take a different approach.
If you insist all pass-by-const-refs should be pass-by-pointer then you've also lost your ability to code-style distinguish mutable vs. non-mutable parameters. Which makes it much easier to make mistakes, not harder. And also litters your code base with meaningless, non-behavior-altering noise.
"Don’t automatically assume that C++’s situation is desirable, necessary, or inevitable."
It seems to be trying to frame this as C++ having bad duplication. But many of the things listed are not actually strict duplicates at all - like pointers vs. references, since references cannot be null (yeah yeah you can force it but 'int& foo = nullptr;' doesn't compile, either). Which if you then read the explanations at the bottom tend to cover in a fairly non-biased way.
void test() {
int* p = 0;
int& r = *p;
}
compiles without complaint.As you've shown, references can be null, but theyre not supposed to be and are assumed nearly universally to not be.
The argument comes down to when the undefined behavior occurs: is it at the deference to create the reference, or is it on the first memory access using the reference? The language pedants will say the former, but in practice, it's the latter.
In practice, you'll likely be able to invoke a member function on a null pointer or reference, as long as that member doesn't directly or indirectly access data members, or virtual functions of the type. Obligatory, I dont recommend doing this or relying upon this behavior, it's just behavior I've seen in my 2 decades of debugging C++.
It doesn't mean a method that takes or returns a pointer must allow null as a valid value, of course, Optional<> is better for that. But standards-enforced non-null is a very practically useful aspect of references that differ them from pointers.
The undefined behavior is always when the null reference is created. The issue will _usually_ manifest when you try to dereference the pointer, but the undefined behavior was creating the null reference in the first place.
But if you do
int& r = *((int*)0);
You'll find it will warn, and tell you that what you're doing is illegal:<source>:2:14: warning: binding dereferenced null pointer to reference has undefined behavior [-Wnull-dereference]
Same if you try to naively return nullptr on a method that returns a reference:
int& iref() {
return nullptr;
}
<source>: In function 'int& iref()':<source>:6:12: error: invalid initialization of non-const reference of type 'int&' from an rvalue of type 'std::nullptr_t'
6 | return nullptr;
| ^~~~~~~
Compiler returned: 1If you get a null reference, this is by chance and not by design.
Although no sane implementation would do this.
And that would be perfectly compiler-legal, and your code would have one free bug.
Yes, that's probably why the article's title is "Near-duplicate features of C++".
> references cannot be null
From the discussion and examples, that seems to be a restriction on the programmer, not a feature of the feature.
Being a restriction and being a feature are not mutually exclusive. Strong typing is a restriction on what's a collection of bits. Functions are just restrictions on your ability to change the PC. Loops are just restrictions on your ability to jump.
Restrictions are necessary to create structure.
as in "it is the programmer's job to make sure this doesn't happen". That's an anti-feature, aka "bug".
What makes you think that?
I'm looking at C++ 17, 11.3.5 (4).
> A parameter list consisting of a single unnamed parameter of non-dependent type void is equivalent to an empty parameter list.
What are you looking at?
I disagree. It does not include a contemporary RNG, and seeding the only decent one correctly is ridiculously difficult.
Objective-C has the same issue, for the same reasons.
Author has a point there, but maybe, just maybe, we won't see "new"s or "delete"s in most future codebases? (we have smart pointers now) Or non-intuitive macros (yay constexprs!)
I see these as stepping stones from C to where we are today without breaking everything at once or doing it with a whole other programming language.
I haven't seen 'new' or 'delete' in the wild for a decade now. (Well, except for various legacy libraries or student code on Github.)
And then you have the fun of passing those to a function. Do you pass by value (shared_ptr) or by reference (I believe you do not) or pass a naked pointer (when you need to pass a unique_ptr to a function that uses, but does not modify or cache the pointer, if I remember correctly). And then suppose you want to give away your unique_ptr, that's fun.
And if you want to deallocate your shared_ptr that's part of a cycle, you'll need to either use a weak pointer or manually reset it.
Apples and oranges. Smart pointers are wrappers over raw pointers. They extend functionality, but are not the same.
> Except that you generally create them with std::make_shared() and std::make_unique()
Again, apples and oranges. You're confusing factory functions that wrap memory allocation for convenience with actual memory allocation.
At most you could only complain that those factory methods duplicate the role of constructors, which would be silly.
> And then you have the fun of passing those to a function. Do you pass by value (shared_ptr) or by reference (I believe you do not) or pass a naked pointer (when you need to pass a unique_ptr to a function that uses, but does not modify or cache the pointer, if I remember correctly).
There is no duplication in your examples. You are just unaware of how those functionalities are used.
And comparing a function pointer to a lambda expression is quite laughable.
Also, this should be viewed as C++ features C doesn't have: It's 2019 C, shouldn't be accepted anymore
On the other hand, I still prefer C to C++ mainly due to the compile time (an incremental compile in a typical large C project I deal with takes a second or so, while C++ takes 3-4 seconds). C++ doesn't have to be slow, as long as you use the needed features. But idiomatic C++ means you'd be using STL, class inheritance, virtuals, operator overloading, templates and constexpr and stuff, all of which contribute to longer compile times.
The obvious solution is to avoid C++ when making new codebase, but that's easier said than done.
Of course there are duplicate features, that's an obvious downside to keeping backward compatibility while still allowing improvement.
If two equivalent/overlapping features were introduced in the same C++ version, then, yes it would be an indicator of a faulty design process.
• I feel that many commenters missed the fact that I was writing about NEAR-duplicate features of C++; I never said they were identical. But these features overlap so much that often all the alternatives are applicable in a real-world situation, and the choice becomes somewhat arbitrary, and you tend to see divergence between different coders and over the years.
• I'm surprised to see that no one said anything like "Indeed, I do see these near-duplicate features in the C++ codebase that I work with. And it bothers me because I had to learn two things instead of one, and we have no stylistic consistency".
• Some people expressed doubts about why I wrote the article. My intent and tone are clearly exhibited in the opening three paragraphs of the article; please reread them fully before judging.
• Many people dwelled on the difference between references and pointers. I firmly support references and use them in my published code. But I've seen many people who learn C++ after C, and they try to use C-style pointers for as long as possible to avoid learning the new, native C++ feature which is much safer. Hence you cannot avoid seeing pointers in C++ if you read/edit code that come from diverse sources.
• Many of my bullet points received no comments. I take it that either no one cares, or no one recognizes these points. For example, is it really sane for a language to have 3 ways of initializing a variable? (int i = 0; int j(1); int k{2};)
• A subtread about const-by-default is something I agree with, though the syntax would mismatch semantics in C. Regardless, Rust is a relatively new language with nearly all of C++'s powerful features, and indeed it implements immutable-by-default.
• I used new/delete in C++ in my early days, as a naive coder coming from C and Java with their manual management of memory allocation. But now I avoid those and rely on std::vector, std::unique_ptr, and RAII, which are safer and more concise.
• (Previous thread: https://www.reddit.com/r/cpp/comments/6clwkq/nearduplicate_f... )