Hacker News new | past | comments | ask | show | jobs | submit login
Near-duplicate features of C++ (2017) (nayuki.io)
77 points by nayuki 33 days ago | hide | past | web | favorite | 57 comments



Some of these absolutely makes sense and are justified; others...are actually sane to have in c++. For instance:

> 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.


I think the problem is mostly in the tone of the opening paragraph and less in the list itself. By prefixing this with

"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.


> are not actually strict duplicates at all

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.


> 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.


"Restriction on the programmer"

as in "it is the programmer's job to make sure this doesn't happen". That's an anti-feature, aka "bug".


> references cannot be null

    void test() {
      int* p = 0;
      int& r = *p;
    }
compiles without complaint.


A failure to issue a warning is not a refutement of the standard. As I said, yes obviously you can force it. You can force just about anything you want in C/C++.

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: 1


This is not a valid program. The second line invokes undefined behavior. The compiler is legally allowed to replace the null dereference with code that sends your porn collection to your mother, and set the reference to 42.

If you get a null reference, this is by chance and not by design.


> The compiler is legally allowed to replace the null dereference with code that sends your porn collection to your mother, and set the reference to 42.

Although no sane implementation would do this.


No, but they may optimize out assigning to the reference at all, since assigning to it requires dereferencing a provably null pointer, which means that any future code is effectively meaningless.


They wouldn't, but they could very well come to a different sane result on a particular architecture than they implementation/architecture you're on.

And that would be perfectly compiler-legal, and your code would have one free bug.


I've gotten into arguments about this in the past.

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's mostly important in the context of codifying nullability. If a function returns a reference it's part of the contract that it doesn't return null. Similarly, if a parameter is a reference it's part of the contract that you can't pass it null.

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 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.

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.


Technically, it's an undefined behaviour. Compilers do what they want in this situation.


> Also, int foo(void) is illegal in c++.

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.


I dont know offhand if it's illegal, but it's certainly unnecessarily verbose. Likewise, every compiler I've used accepts it, but it's unnecessary. Also, maybe a difference in behavior if code is in 'extern "C"' or not. I'm not sure. I rarely directly deal with C libs and havent been doing C++ for about 6 months now.


Unnecessary is a question of taste, right?

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.


> 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?

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).


const by default: I think I explained badly or you read too much into it or both. This is pure syntactic sugar I'm talking about. Instead of marking const member functions const, you mark functions which aren't const "mutable." Literally no change to the semantics of the language, just which one is assumed if you leave it blank. Right now it's mutable by default if you do nothing, it could just as easily have been const, you need to be able to tell the compiler which one you mean with a keyword whichever one is chosen to be represented by no token.

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.


> If "everything should be const unless it has to be mutable" is a reasonable position to take

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.


We're just going to have to agree to disagree on that. Const by default would have saved me quite a few headaches over the years and C++ syntax is the definition of cluttered. Good C++ code has the majority of function parameters marked const. A non const reference member function parameter is usually a warning to look carefully, isn't it?

"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.


mutable already exists and means something different from non-const.

   class foo {
     void b1();
     void b2() const;
     void b3() mutable;
   }
b1, b2, and b3 are all distinct.


Distinct indeed, they've got different names. If they're all called b1() then what? But you're hung up on "mutable" as a keyword. What is the semantics of b1(). The language could make them const semantics instead and so also provide a different keyword for 'non-const' maybe 'nonconst' would do if it really clashes with 'mutable' (I don't care if it is, I'm not proposing this to the committee as it's insane to be incompatible with existing code). When it was all just a twinkle in Bjarne's eye, he could have gone that way. Should have, say I.


That's no different to a class having a pointer to memory outside the class, which is modified by the const member function.


I largely agree with what you say, but can you elaborate on what you mean by "macho coding" styles? I'm guessing youre referring to when one deigns venture into template metaprogramming. TMP is an area with 2 decades of experience, I can basically only do it with a copy of the standard and/or relevant reference material handy. Although I'd consider my C++ knowledge in the 95th percentile or so, I probably know less than 20% of the std lib well. I've read most of the +1200 pages of the C++11 spec, but ive not kept up to date since, sadly, because its no longer my daily language.


What I mean is anything that is showing off, has a lack of humility and implicitly disparages others in a way that is totally unneeded while producing lower-quality, less-maintainable, less robust code. Being clever for the sake of appearing clever. That kind of thing.

    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... )


> 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?

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)


when you read a function call

    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++.


> 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?

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.


Rust is a relatively new programming language that features nearly all the good parts of C++ (e.g. struct, reference, RAII, move semantics, generics) and almost none of the bad parts (e.g. duplicate features, cryptic syntax, undefined behavior).

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);


You shouldn’t need the type annotations on the three variables, by the way.


> Also, int foo(void) is illegal in c++.

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?


Ah, my mistake. Still, though, it's unidiomatic and rarely (if ever) used.


> libc++ random is actually decent.

I disagree. It does not include a contemporary RNG, and seeding the only decent one correctly is ridiculously difficult.


Afaik it uses mt19937? While not as good as it could be; something like pcg would probably be better, that's still very adequate. As for seeding it's pretty easy. It is stupid boilerplate that should be in the stdlib, but it's not difficult. You read from /dev/urandom if on unix, or use cryptgetrandom on windows, xor with time and pid if you really want to, and that's it.


The tone of this is a bit silly. The main reason C++ was a success was due to the backward compatibility with C. That backward compatibility has (fairly obvious) downsides.

Objective-C has the same issue, for the same reasons.


Well, one might grok C++ with the same perspective the author had, but I think C++, and especially C++17, make the language much safer and friendlier for beginners. C++17 for example is a big enough departure from pre C++11 days. If you stick to the bleeding edge, it doesn't look anything close to C at all.

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.


> maybe, just maybe, we won't see "new"s or "delete"s in most future codebases?

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.)


Me too. I've been writing pretty much only C++ for 20 years, and if I'd go back and count, I think I'd have maybe a dozen times where I used new/delete, none of those in the last 10 years. Like all languages that are actually used, there is much one can complain about in C++, but claiming that one needs a lot of manual memory management is done mostly by people who haven't written much if any C++.


Or people who care about heap fragmentation and cache misses.


Sure, but those people don't want GC either. Not to say that all languages that are not C++ are garbage collected, but the people who complain about manual memory mgmt are not the ones who care about those things.


Qt code uses 'new' extensively because Qt components take ownership of components and manage memory allocation automatically.


What you describe is still common practice in the language, but the mechanisms no longer use a new or delete to achieve that.


The author missed an opportunity on the heap allocation section. Now you aren't even supposed to do `new`, but rather std::shared_ptr<...> or std::unique_ptr<...> (as appropriate). Except that you generally create them with std::make_shared() and std::make_unique(), so that adds another 4 items there.

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.


> Now you aren't even supposed to do `new`, but rather std::shared_ptr<...> or std::unique_ptr<...> (as appropriate).

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.


C++ is a more type safe language than C, and some of these “duplicated“ features are actually adding safety to the C versions. Most of these “duplications“ are quite in line with the C++ philosophy and are there for a good reason, reducing countless bugs and making code safer.

And comparing a function pointer to a lambda expression is quite laughable.


Duplicate features, and thank god they exist: References, for example, have probably stopped millions of pointer arithmetic bugs of the years.

Also, this should be viewed as C++ features C doesn't have: It's 2019 C, shouldn't be accepted anymore


Reference is among one the things I really want to be included in C. Does it break anything if it's introduced to the standard?

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.


I'd prefer longer compile times over runtime issues: Smart pointers are cheap, and easy to use now. C++ compile times are largely due to the absolutely ridiculous import system C++ forces you to use. Precompiled headers can eliminate a huge amount of C++ compilation time.

The obvious solution is to avoid C++ when making new codebase, but that's easier said than done.


Herb Sutter has said again and again that his goal is to create a language that allows you to write less even though the committee is adding stuff. this fits with their goal of backwards compatibility. I don't see an issue


The intent of the author is not very clear.

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.


Thanks to everyone for the feedback. Here are my aggregated responses:

• 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... )


From the definition of the backward compatibility you get "old features still work while the new features that do the same thing work too".


Say what you will about c++, but it's the only language I know where you can declare classes as "friends" of one another.


It’s there for very rare purposes, and it’s considered a code smell if you ever use that feature.


Updates/upgrades are justified. Using old style in new code, often, isn't.




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

Search: