Thanks to comments on my blog post asking for suggestions about how to name the book, I've changed the working title to Effective Modern C++, and I've accustomed myself to distinguishing the abbreviation EMC++ (the current book) from MEC++ (More Effective C++). Sharp-eyed readers may have noticed that the new title has already appeared on a talk I'll be giving in June at the Norwegian Developers Conference.
I recently finished the 32nd Item for the book, thus giving me drafts of five full chapters. The math still shows that about 40 Items will fit in the book's allotted 300 pages, so yesterday I took a hatchet to the prospective table of contents and chopped the number of Items down from 51 to 41. (Why 41? Because I have a feeling that one of the Items I've written will eventually get jettisoned as not being important enough to make the final cut.) Here's the current draft TOC. The chapters I've written are 1-4 and 6 (i.e., chapters 5 and 7 remain on my todo list).
Chapter 1 Deducing Types Item 1: Understand template type deduction. Item 2: Understand decltype. Item 3: Know how to view deduced types. Chapter 2 auto Item 4: Prefer auto to explicit type declarations. Item 5: Remember that in variable declarations, auto + { expr } yields a std::initializer_list. Item 6: Be aware of the typed initializer idiom. Chapter 3 From C++98 to C++11 and C++14 Item 7: Distinguish () and {} when creating objects. Item 8: Prefer nullptr to 0 and NULL. Item 9: Prefer alias declarations to typedefs. Item 10: Prefer scoped enums to unscoped enums. Item 11: Prefer deleted functions to private undefined ones. Item 12: Declare overriding functions override. Item 13: Prefer const_iterators to iterators. Item 14: Use constexpr whenever possible. Item 15: Make const member functions thread-safe. Item 16: Declare functions noexcept whenever possible. Item 17: Consider pass by value for cheap-to-move parameters that are always copied. Item 18: Consider emplacement instead of insertion. Item 19: Understand special member function generation. Chapter 4 Smart Pointers Item 20: Use std::unique_ptr for exclusive-ownership resource management. Item 21: Use std::shared_ptr for shared-ownership resource management. Item 22: Use std::weak_ptr for std::shared_ptr-like pointers that can dangle. Item 23: Prefer std::make_unique and std::make_shared to direct use of new. Item 24: When using the Pimpl Idiom, define special member functions in the implementation file. Chapter 5 Lambda Expression Item 25: Avoid default capture modes. Item 26: Keep closures small. Item 27: Prefer lambdas to std::bind. Chapter 6 Rvalue References, Move Semantics, and Perfect Forwarding Item 28: Understand std::move and std::forward. Item 29: Distinguish universal references from rvalue references. Item 30: Pass and return rvalue references via std::move, universal references via std::forward. Item 31: Avoid overloading on universal references. Item 32: Understand alternatives to overloading on universal references. Item 33: Understand reference collapsing. Item 34: Assume that move operations are not present, not cheap, and not used. Item 35: Familiarize yourself with perfect forwarding failure cases. Chapter 7 The Threading API Item 36: Make std::threads unjoinable on all paths. Item 37: Specify std::launch::async if asynchronicity is essential. Item 38: Be aware of varying thread handle destructor behavior Item 39: Consider void futures for one-shot event communcation. Item 40: Reserve std::atomic for concurrency, volatile for memory-mapped I/O. Item 41: Employ sequential consistency if at all possible.
You may want to compare this to the initial preliminary TOCs I posted here and here to confirm that I wasn't kidding when I said that things would change. Things might change in the above TOC, too, but this one will be a lot more stable than the earlier versions.
The most recent Item I wrote was "Distinguish () and {} when creating objects." I blogged about one aspect of this topic here, and I thought you might be interested to see what I came up with. I've therefore made the current draft of this Item available, and I welcome your comments on it. Like almost all Items I've written, it's too long, so I'm especially interested in suggestions on how I can make it shorter, but I welcome all suggestions for improvement.
As things stand now, I'm hoping to have a full draft of the book by the end of April. We'll see how it goes.
Scott
53 comments:
Couldn’t Item 5 and Item 7 be combined into one?
Looking forward to this heaps. Sounds awesome.
I like the fact that Chapter 4, Smart Pointers, recommends std::unique_ptr before std::shared_ptr.
I'd go even further and _explicitly_ recommend automatic(-like) variables (including RAII-style objects) even _before_ recommending std::unique_ptr (perhaps it's also worth to even consider slightly refactoring the chapter to something like "Automatic Resource Management" in general?).
Something like "Item 19: Before using smart pointers, remember to carefully consider whether dynamic storage / free store is at all necessary. Chances are it's not, and it always introduces considerable complexity to your code base -- which the smart pointers will partially alleviate, but not eliminate. Come to think of it, on the off chance you think you need it because you need dynamic polymorphism, you should go ahead and reconsider that, too." // Yes, the title could probably be a bit shorter... probably ;-)
Rationale: in terms of full disclosure, I admit to a bit of a self-interest here: it would be _extremely_ helpful and convenient to be able to explicitly point out a rule like this to fellow programmers who will now (or are about to) use smart pointers all over the code base ("because it's all automatic, so it's easy & simple, and nothing could possibly go wrong, right?"; "oh, this is cool, now I can introduce fancy class hierarchies all over the place, because the only inconvenient thing about them was manual memory management, and that's now gone, so they must be OK now, too!" after all, an hour long session spent pointer chasing in a debugger can be so much fun :]) -- and, worryingly, by implication may resort to relying on free store even more than necessary (compared to the days before, when we all (well, most of us) knew it was nasty stuff ;]).
In a way, this is a part of the natural hierarchy that fits with with the parts you've already delineated explicitly -- just like it makes sense to consider std::unique_ptr before std::shared_ptr (because unique ownership is simpler and often "just enough to be just right" compared to shared ownership), it also makes sense to first consider scope-based resources/variables (which, as a special case of unique ownership, are even simpler).
What do you think?
It could certainly make life easier :-)
@P: Item 5 spends time talking about the interaction of braced initializers and auto in contexts other than object creation, e.g., in function return type deduction. That information is important, IMO, and it fits in better with the chapter on type deduction. Also, Item 7 is already too long, so merging it with Item 5 would yield an Item that was way too long.
@Matt: Smart pointers are the alternative to raw pointers, and that's the context in which I discuss them. If your developers are using heap-based resources when they don't need to, that's a different kind of problem, I think.
Hi Scott, personally I think the confusion with brace-initialization is one of the most frustrating things when trying to stick to a really "uniform" way of doing things, so this item is one of the things I really hoped to find in the book.
I find it extremely clarifying, I just have one doubt: when at the very beginning you say I'll generally ignore the braces-plus-equals-sign syntax, because C++ usually treats it the same as the braces-only version
isn't really (as per 13.3.1.7) there a subtle difference related to the copy-list-initialization form (i.e. the one with the '=') being ill-formed in case an explicit constructor gets selected?
Really looking forward to get my hands on your new book asap!
I'll be interested to read 'Item 16: Declare functions noexcept whenever possible' because it goes against what I've gathered. My understanding is that noexcept by and large should only be applied to special member functions and swap.
For example Jon Kalb's 'exception safe coding' guildlines (http://exceptionsafecode.com/) say to use noexcept on cleanup routines, move/swap, and nowhere else.
Hurry up because I want to give you my money! ;)
@Seth: I posted a draft of "Use noexcept whenever possible" in this earlier blog post. Take a look and let me know what you think.
@abigagli: There's a reason for the word "usually" in the text you cite :-)
Like Seth, I was surprised to see such a guideline as Item 16, but having read the draft (where it's numbered 19), I realized that it's just the title which is misleading (at least for me).
The key part is: "Exception-neutral functions aren’t noexcept, because exceptions may pass through them. **Most functions, therefore, aren’t noexcept**."
Further on: "Some functions, however, are naturally noexcept, and for a few more — notably the move operations and swap — being noexcept has such a significant payoff, it’s worth implementing them in a noexcept manner if at all possible. **When you can honestly say that a function should never emit exceptions**, you should definitely declare it noexcept."
So in fact, the "whenever possible" wording pretty much amounts to "those few rare (yet important) situations where it does make sense".
So how about replacing "whenever possible" with "whenever appropriate, but not more often"? I'm sure you can come up with something better.
Also, a number of items equal to 42 wouldn't be bad :)
Looking forward!
@Andy: The problem with "Declare functions noexcept whenever appropriate" is that it's not just wishy-washy, it's also kind of tautological. Reasonable programmers should do everything when it's appropriate, right?
In the case of noexcept, the advice I'm giving is a departure from C++98, where it was generally a bad idea to use exception specifications, so I want to emphasize that when a function can reasonably be declared noexcept, it should be declared that way--that failing to declare it that way incurs a performance penalty.
I suppose that people might interpret "Use noexcept whenever possible" as "throw noexcept on all your functions, regardless of whether it makes any sense," but I'd hope that after reading the corresponding Item body, they'd know better. An Item title is designed to summarize the advice of an Item, and I word Item titles quite carefully, but I expect people to read the entire Item, not just its title.
Scott
Okay, the draft of 'Item 16: Declare functions noexcept whenever possible' turns out to be more in agreement with my previous understanding of proper noexcept usage than I had expected from the title.
I do believe the title and the early comments about the optimization benefits:
"This alone should provide sufficient motivation to declare functions noexcept whenever you can."
may encourage people to apply try to apply noexcept more than they should.
In particular my concern is that people will apply noexcept in inappropriate places by adding explicit error checking (either try/catch or by trying to eliminate exceptions entirely, moving to error codes), not realizing that the optimization enabled by noexcept may not make up for the performance lost in all the explicit error handling code they add.
I think modifying the title to something like "Know when to declare functions noexcept", softening the above quote ("whenever you can" sounds pretty much like "always" to me), and perhaps giving more emphasis and discussion to the comment at the end about how most functions should not be declared noexcept, would result in more readers getting the right take-away point from this item.
Hi Scott, very much looking forward to your new book!
On the topic of braced initializer syntax, on the standards committee, do you know what the reasons were for not making the std::initializer_list construction always explicit and separate from the passing of the list as an argument to another constructor? E.g., require std::vector x{{1, 2, 3}} to select the init list overload. Wouldn't this resolve most of the issues with uniform initialization and generic code?
@Christian Rorvik: I don't know the reasons why the rules regarding the various forms of initialization are in the form they are, sorry. As I've blogged about elsewhere, I can't even find out why there's a special rule for auto and braced initializers.
@Seth: Your concern that people might start warping function implementations in order to declare them noexcept is one I had not considered. I'll consider adding something to address that possibility. But it's a funny notion. For example, I advice people to use const whenever possible, and I haven't heard about people adding const for no reason and then using casts to work around the constraints that that imposes. But perhaps the situation with noexcept is different.
Yeah, I do think the situation with const is different. Advising to use const 'whenever possible' is okay because there's a pretty obvious answer to the question 'when is it _not_ possible': When you need to modify the thing. (and it's obvious that just const_casting const away directly negates the value of declaring things const in the first place, besides being dangerous except under special circumstances.)
But given the correspondingly obvious answer to the question 'when is it not possible to use noexcept,' (when you need to throw exceptions), people more readily find alternatives to throwing exceptions than they find for modifying objects.
To put it another way, converting
void foo() { bar(); }
foo();
into
error_code foo() noexcept {
try {
bar();
} catch(...) {
return error_code(-1);
}
}
if (!foo()) {
// handle error
}
may not look all that unreasonable to everyone, whereas const_cast always looks strange (even in those cases where it's arguably okay).
(Also, if someone took 'use const whenever possible' more strongly and decided to write programs in a more functional style as a result, that might not actually be bad.)
@Christian Rorvik "Wouldn't this resolve most of the issues with uniform initialization and generic code?"
No, it would just result in different issues. Instead of needing to know the difference between () and {}, you'd need to know the difference between () and {} on one hand, and {{}} on the other. Initialization wouldn't be any more uniform.
The answer to the possible ambiguity between using () and {} is to never design a class that exhibits such ambiguity; Never add a constructor such that passing arguments via () has different results from using {}; Never add a constructor that can only be accessed via () syntax.
The problem with the above is that the standard library violates it in a few places due to backward compatibility requirements. The best we can expect in terms of fixing that is that some constructors are added so that we can write, for example:
std::vector v{std::length, 10};
in order to enable eliminating the use of () constructors.
@Seth: I think you make a good point. I'll add something about not missing the forest for the trees as regards declaring functions noexcept.
I think the noexcept recommendation should be softened to something like "Mark functions that never fail as noexcept".
This would reflect the Committee's conservative stance on `noexcept` specifications to those functions with "wide" (see N3248/N3279 for the terminology) contracts (i.e. without any preconditions on their implicit/explicit arguments).
E.g. in the N3242 C++0x draft Standard, the `front()` member function of `std::string` was marked as `noexcept`, even though it has a precondition `!empty()`. In all later drafts, the `noexcept` specification on this and similar functions has been removed. One issue is that testing the precondition with a throwing `assert(!empty());` inside `front()` would be impossible because test drivers would fall victim to a `std::terminate`.
The Standard Library has only functions with a wide contract marked as `noexcept`, and functions with a "narrow" contract can either specify the exception(s) thrown upon violation of the precondition(s) or they can document "Throws: nothing".
This still leaves the gist of your item of having noexcept move/swap for optimization purposes.
@Seth
>>No, it would just result in different issues.
What issues?
>>Instead of needing to know the difference between () and {}, you'd need to know the difference between () and {} on one hand, and {{}} on the other. Initialization wouldn't be any more uniform.
You'd need to know only two rules - use {{}} when you want std::initializer_list constructor and use {} everywhere else (including templates). () would become outdated. Pretty uniform, isn't it?
>>Never add a constructor such that passing arguments via () has different results from using {}
This rule is absolutely artificial and takes place only because the current rules are already locked in the standards and therefore braced initialization is crippled forever and now you can only mitigate the harm.
@Rein Halbersma: My original outline for EMC++ had an Item on narrow and wide interfaces, but I was warned away from it on the advice of a committee member who said that the committee's enthusiasm for the idea was not widespread. How widespread that assessment of the situation is, however, I don't know.
Regardless, I still think my advice is reasonable. If you have a function that you are willing to guarantee will never emit exceptions, I think it's good practice to declare it noexcept. (I've been wrestling with the implications this has for constexpr functions.)
BTW, "will never emit exceptions" is not the same as "will never fail." There are ways to communicate failure other than exceptions (e.g., status codes).
Like Seth and Andy, I had some trepidation about Item 16 when I read the title. As Seth points out it seems to contradict my recommendation in ESC and I'm not excited about defending recommendations that contradicts EMC++.
However, when I read the posted article in full, I didn't see anything I couldn't get behind. I think the danger is someone reading (and following) the guideline without reading reading the whole article. To the extent that that happens in general we are all doomed anyway.
I think when I get to this guideline in presenting ESC from now on, I'll probably say something like, Scott's advice sounds very different from this, but if you read his entire item (recommended), you may find that our positions aren't that different.
In presenting this item in ESC, I've always done it with some humility. I've cautioned that I'm make the recommendation as a prediction, because this is an area where a few years of legacy C++11 code will yield valuable experience and we just don't have that yet.
@Vladimir
>>> You'd need to know only two rules - use {{}} when you want std::initializer_list constructor and use {} everywhere else (including templates). () would become outdated. Pretty uniform, isn't it?
No, this suggestion just shuffles around which constructors are called by which syntax without solving the problem..
Under this suggestion passing a temporary to any constructor would be ambiguous with constructors that take an initializer list, because the temporary would use a second set of braces, just like initializer_list.
For another thing, initializing a vector under this suggestion is not uniform with initializing an array.
I think the title "Know when to declare functions noexcept" would work well. It's not wishy-washy or tautological. I think it nicely communicates the right implication.
Anyway, the book looks like it's coming along really well. I'm looking forward to getting it.
@Seth: The problem with "Know when to declare functions noexcept" is that, IMO, you should do it whenever you can, hence my current (but still tentative) Item title.
There's enough meat in this topic to warrant an independent blog post, so I plan to make one devoted to noexcept functions later this week. I plan to post the current draft of that Item at the same time. The draft that's online now is several weeks old, so what I have now isn't quite the same. Also, I want to find a way to address your concern that people might warp their function implementations simply to make it possible to use noexcept.
I hope to convince you that my Item title is reasonable, but perhaps you (and others) will convince me that a different title would be preferable. Right now, I honestly don't see what makes "Declare functions noexcept whenever possible" different from "Use const whenver possible" and "Use constexpr whenever possible." But maybe I'm just a slow learner.
@Scott:
>> Right now, I honestly don't see what makes "Declare functions noexcept whenever possible" different from "Use const whenver possible" and "Use constexpr whenever possible."
There are two things that make "use const whenever" a good and viable recommendation: 1) compiler checking and 2) no difference between Debug and Release mode. Small code example:
http://coliru.stacked-crooked.com/a/a33130b0184f3898
For constexpr, the -Winvalid-constexpr for g++/clang gives similar compiler support as with const when a constexpr function tries to call a non-constexpr function. But for noexcept there is no such warning mechanism (not to my knowledge at least).
Both constexpr and noexcept differ from const in that Debug/Release implementations might differ in their ability to satisfy the interface (checked iterators, anyone?).
The new C++14 relaxed constexpr illustrate this dilemma: a class template like std::bitset could be made constexpr wholesale, except for the fact that this eliminates Standard algorithms and lambda expressions from the implementation. But even if Standard algorithms would be marked constexpr, then the Debug versions with checked iterators would still fail to comply.
Maybe that's one of the reasons that the Committee does not allow library implementers to add constexpr anywhere, even though they do permit adding noexcept.
@Jon: do I understand your guidelines correctly in that you recommend never to add noexcept, but for move/swap? Not even for simple getters that return a builtin by-value (a size() member function comes to mind)?
@Seth
>>No, this suggestion just shuffles around which constructors are called by which syntax without solving the problem..
The problem is the syntactic conflict between initializer_list (il) constructors and non-initializer_list (n-il) constructors.
To shuffle things around is to make vector{1, 2} call n-il constructor, but keep vector{1, 2, 3, 4, 5} well formed and calling il constructor. My suggestion (not only mine actually, it's a relatively common suggestion) was to make vector{1, 2, 3, 4, 5} ill-formed, thereby eliminating the syntactic conflict completely.
>>For another thing, initializing a vector under this suggestion is not uniform with initializing an array.
And that is good, because initialization with il constructor and aggregate initialization are absolutely different things. (Nonetheless, std::array can be initialized with double braced syntax too, just as std::vector.) Even if it's not good from someone's point of view, it's a small price compared to uniform initialization usable everywhere without syntactic conflicts.
>>Under this suggestion passing a temporary to any constructor would be ambiguous with constructors that take an initializer list, because the temporary would use a second set of braces, just like initializer_list.
Well, that's a real issue. I can't quickly come up with a solution, probably some additional parenthesizing of the initializer could be allowed for such an emergency situations. But I again can argue that it's much lesser issue compared to, for example, using braced initialization in templates.
Anyway, it's useless to argue now, because nothing can be changed (but I'm still interested :))
@Seth
Small addition:
Under current rules passing a temporary to a constructor conflicts with il constructor too (with the same outcome as in the hypothetical situation - il constructor is selected)
@Vladimir
>>> And that is good, because initialization with il constructor and aggregate initialization are absolutely different things.
The entire point of introducing initializer_list was to make it so users could make their class initializable using the same syntax as built-in arrays.
Furthermore one of the goals of uniform initialization in general was to use the same syntax with both aggregates and constructors.
The rules for developers I proposed above work with C++11 as it is and achieve the goal of a _single_ syntax.
If you're interested and haven't already read it, you might take a look at Bjarne's paper:
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2532.pdf
That same year also has a number of other papers following the development on initializer lists.
@Jon Kalb: I took a look at your ESC slides, and I was taken aback by what appears to be your recommendation to restrict noexcept to move and swap functions. I'd be interested to know the technical basis for this recommendation. For example, if I have a non-inline function that uses only the C subset of C++ and that I believe will continue to be implemented in that fashion, why would I want to give up the optimizations in callers that using noexcept could enable?
@Vladimir: Can you please elaborate on what you mean when you say that under current rules, passing a temporary to a constructor conflicts with il constructor? I'm not familiar with any conflict that arise only for rvalue arguments.
Thanks.
@Seth
>>The entire point ... class initializable using the same syntax as built-in arrays.
Which led to the current sad situation.
>>The rules for developers I proposed above work with C++11 as it is and achieve the goal of a _single_ syntax
I'm not saying the rules are not true, I'm saying they have no semantic motivation and exist only because of the syntactic quirks.
>>http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2532.pdf
I did read this particular paper (and a couple of others, but not all the sequence) but not especially carefully. I've reread it today.
Meanwhile I actually found flaws in my own reasonings. But I still think the current rules are flawed too. Hm, should probably investigate more. Thanks for the discussion.
@Scott Meyers
Sorry for spamming your blog with our prolonged conversation
@Scott Meyers
I've repeated Seth's words from "Under this suggestion passing a temporary to any constructor would be ambiguous with constructors that take an initializer list, because the temporary would use a second set of braces, just like initializer_list." but the words aren't quite precise. The next situation is implied (if I got everything right):
struct A1
{
A1(int, int, int) {}
};
struct A2
{
A2(A1) {}
A2(std::initializer_list) {}
};
int main()
{
A2 a2{{1, 2, 3}}; // {1, 2, 3} is supposed to be a "temporary" of type A1
}
Fix to the previous post: A2(std::initializer_list<int>) {}
@Scott Meyers
There's no guarantee that C functions won't throw exceptions. glibc actually implements POSIX cancellation points with exceptions, so an exception can be thrown by fopen(), for example.
Of course your question stands for code you really know won't throw. In my opinion the answer here hinges on whether or not the cost of identifying this code is really worth the benefits gained through the optimization.
I don't have any real data to back this up, but I suspect that in practice the small optimizations enabled by using noexcept are usually not, on their own, sufficient to justify the cost, and that instead the additional benefits that obtain in the case of move/swap and cleanup code are usually required to justify it.
---
Jon Kalb does briefly address this in his talk. You can see what he says at 40:25 in the part 2 video on his site. To summarize, he thinks the risk of mistakes during future modifications is significant enough and the enabled optimizations probably aren't great enough that if he doesn't need noexcept then he'd rather not risk it.
So, no new information, just a different judgment call on the relative values.
---
Here are a couple emails where compiler devs discuss performance implications relevant to noexcept:
http://lists.cs.uiuc.edu/pipermail/cfe-dev/2013-February/027966.html
http://lists.cs.uiuc.edu/pipermail/cfe-dev/2013-October/032636.html
Related to "Item 25: Avoid default capture modes".
Probably this refers to possible dangling references. This is not specific to default capture modes. Also when you explicitly capture "this" or capture local variables by reference and return the lambda somewhere you have similar problems. I would change the item to "don't let lambda expressions outlive the life-time of the captured variables" or "Be careful when capturing this (implicitly or explicitly)".
Related to Item 30: Pass and return rvalue references via std::move, universal references via std::forward.
Make sure that people don't get the wrong take-away. From "return rvalue references via std::move" people might get the impression that is a good practice to write return std::move(...). Often this is not, first of all because it is most of the time not necessary: move is done implicitly if local values are returned. Moreover, Return value optimization and Named NRO disabled when you write "return std::move(...)". See also Howard Hinnant's discussion on Stack Overflow: http://stackoverflow.com/a/4986802/1274850
@Bert Rodiers: Regarding Item 25, I actually think that a default by-value capture mode is more dangerous, because it lulls you into thinking that your closures are self-contained, but uses of non-locals (including data members implicitly accessed via this) may continue to dangle. The problem with the this pointer is specific manifestation of a broader issue.
Regarding Item 30, note that I'm referring to returning universal references, not objects. For objects, applying std::move in return statements is generally wrong (and applying std::forward always is), but for urefs, applying std::forward is generally correct (as is applying std::move to rrefs). It's important to distinguish between objects and references in this kind of discussion. In my Item, I address both.
I finished reading the piece on initializers and I think there are two things making it far too long. I gave a talk at work on initialization and covered what yuo have and more and barely crammed it into 1 hour. You have an item on auto x = {5} and an item on curly brace initialization. you're right to not merge them, but I would suggest take all the references to initializer lists out of 7 completely and merge that with 5. 5 should then come after 7. In 7 you can put in a "However, read the next item for more."
The other thing I notice is that you spend more time than usual for your series explaining what they are. What I've enjoyed about your series is you assume we are smart enough to know what language features are and if we don't we can look them up.
I hope this helps. It looks like pretty good coverage otherwise.
@James Edwards: Thanks for your comments. As it happens, Item 5 has now been turned into an Item devoted to auto type deduction (including in contexts beyond variable declarations, e.g., function return types and lambda parameters) and been added to the type deduction chapter, so the Item on parentheses vs. braces (the posted sample Item 7) is the only place I discuss the special overloading resolution rules for constructors. IME, people find them counterintutive, so it's worth explaining.
I'd be interested in examples of topics I explain that you consider too elementary. I'm happy to leave information out that people should know or can easily look up, but I also try to include background information on language features that, in my experience (or at least in my opinion), readers are unlikely to be familiar with. It's hard to know where to draw the line.
Currently, lines 3-16 on page 4 are redundant with information in the Item on auto type deduction, so I could eliminate the examples (lines 11-16), but lines 3-7 introduce what's coming up (special constructor overloading resolution rules), and lines 7-10 comprise a cross-reference to auto type deduction that needs to remain in any case, so only about a quarter page is arguably superfluous, IMO. On the other hand, authors think all their words are precious, so maybe I'm just being blind to material that can go. As I said, I welcome examples.
Obviously this is just an opinion, but lines 12-23 of page 1 could be written in the present tense and be in any of your previous Effective books. Line 18 will still cause confusion and I still get programmers shocked by it, but nothing in C++11 changes that, so it feels superfluous. But I did learn there's a "confusing mess" lobby, so that's good. As part of the setup, the mention of std containers is important, so some brief explanation of that problem still needs to be explained.
Page 3, lines 6-21 I think is another example. It's a very nice explanation of a C++ 98 issue, but I think it waters down the meat. A quick illustration of the issue and how it's now solved seems better.
I may not be the target audience, though I plan on buying the book, but it feels to me like too much time is spent looking back, and not enough looking forward. I would assume, but you know assume, that new programmers will do what I did with your first book, read the headline, apply it in practice, and re-read until I not only get it, but it has become intrinsic to my code and I can confidently break the "rules" knowing exactly why it's OK.
Cheers.
@James Edwards: Funny you should mention the material on the ambiguous nature of "=". I added it based on somebody else's comment on a different blog post. I agree that it's a C++98 thing, but I also agree that many people continue to be unaware that "=" need not imply assignment, so I'm inclined to keep it. As usual, there's no way to keep everybody happy. If you know about the issue, my coverage is superfluous. If you don't, it's not.
The material on the most vexing parse needs to be present in some form, because it's one of the arguments in favor of using braces, and I also think that people coming to C++ from other languages are especially susceptible to confusion over the sometimes-you-need-them-and-sometimes-you-don't nature of parentheses when creating objects. Having covered it in only 16 lines, I thought the treatment was pretty quick :-) Of course, the fact that I include such "quick" treatments is one of the things that make the current Item drafts too long and not as focused as perhaps they should be.
In Item 7, I like that you mention that the {} syntax allows you to initialize an STL container with a set of values. In my mind, the key feature of braced initialization is that it allows the initialization syntax for C++ containers to have symmetry with C array/struct initialization syntax.
Regarding the paragraph about the ambiguous nature of '=', what about going the other way? You mention that "for built-in types like int, the difference is academic." Correct me if I'm wrong, but shouldn't the same hold true for any class that properly implements the assignment operator?
I agree with James Edwards that you probably don't need to describe the most vexing parse again. What about just quickly mentioning that {} syntax avoids the most vexing parse and then referencing the Effective STL item?
@Alex Howlett: It depends on what you mean by "properly"? If you check for assignment to self in your assignment operator, that's a test that's not performed in a constructor, and hence the instructions executed for assignment are not the same as for construction. In general, assignment and construction for user-defined types are not the same, but for built-in types, they always are.
As regards the most vexing parse, your view is that I should essentially eliminate lines 7-21?
That's a good point about the performance of assignment vs. copy initialization. What I meant by "properly" is that the assignment operator is implemented in such a way that you end up with the same object regardless of whether you copy initialized it or or default constructed it and then assigned to it. Is there a big downside to a newbie being misled into thinking an assignment is taking place?
And yes, I don't think it would hurt to eliminate lines 7-21.
You could also simplify the item by leaving out copy initialization (with equals sign) entirely and only using direct initialization (without equals sign). Then, instead of comparing three different forms of initialization, you'd only be comparing two.
You could then write a separate item that discusses the difference between direct initialization and copy initialization. This is where you could mention the std::atomic example from page 3.
In your previous books, I'm used to each item representing a succinct nugget of advice or rule of thumb. I'm not getting that vibe from item 7. Is it necessary to enumerate all the differences between parentheses-based initialization and braced initialization? Or could it make more sense to give a single general piece of advice that helps people intuitively navigate the differences?
For example, if we treat values enclosed in {} as "lists of elements," it becomes obvious that make_unique and make_shared *should* use parentheses internally. The behavior of auto is intuitive rather than surprising if you think of {} as a list that's convertable to the type of its single element. The fact that {} prevents narrowing conversions is then a consequence of "boxing" the value in a list.
It also explains why this works:
for (auto i : {1, 4, 9})
std::cout << i << ' ';
I feel like your example on page 2 lines 9-15 is very similar to the most vexing parse. In either case, you can use the {} syntax as a workaround. Using {} in this way feels very hacky to me even though I know Bjarne favors it. I like that you distinguish between "braced initialization" as a syntactic construct and "uniform initialization" as a feature of that syntactic construct.
Whew. This got long. Here's part 2:
On page 4 when discussing constructor calls, instead of saying:
"In constructor calls, parentheses and braces have the same meaning as long as std::initializer_list parameters are not involved."
you could say:
"Empty list initialization calls the default constructor. Non-empty list initialization calls a matching list constructor. In the absence of a suitable list constructor, the elements of the list are considered as arguments for overload resolution on the other constructors."
Everything about the "compiler's determination to match braced initializers" makes perfect sense with this interpretation. And I don't really feel like the empty list initialization warrants being called out as a twist on pages 6-7.
On page 7, I'd be okay with it if you left out your second "twist" entirely along with its example. As far as arcane rules about braced initializers goes, I don't think this particular one will matter on a day-to-day basis.
On page 8, I think you hit the nail on the head with the std::vector example. The standard containers are where {} vs () is most likely to trip people up.
For the first takeaway, more succinct advice could be:
"Only add list constructors to classes that represent containers and only for the purpose of initializing their elements."
Also, I can't think of when it would be a good idea to add a list constructor without also adding a list assignment operator. Rule of 2?
On page 9, a more succinct second takeaway could be:
"When using containers, braced initialization provides an initial list of elements."
On pages 10-11, I feel like the doSomeWork example should *always* use parentheses internally because you can still call the list constructor by calling doSomeWork like this:
doSomeWork(v, {10, 20});
The reverse is not true. You can't get the "normal" constructor if you use braces the template body. I can't think of an example for which it would be preferable to use braces.
@Alex Howlett: Thanks for your extensive remarks. My only comment is that
doSomeWork(v, {10, 20});
won't compile, because you can't perfect-forward braced initializers. That's why you can't use std::make_unique or std::make_shared to create an object using list initialization.
Posting again with angle brackets fixed:
Wow. Good point. I could have sworn I'd done something like that before, but I guess not. I wonder why they chose to deduce braced initializers to std::initializer_list for auto, but not for templates.
I suppose you'd have to do this:
doSomeWork(v, std::initializer_list<int>{10, 20});
Using parentheses internally still allows the caller to choose the list constructor.
For example, I just tried this in both GCC 4.8.1 and Visual C++ 2013 and it worked as expected:
auto vec1 = make_shared<vector<int>>(initializer_list<int>{10, 20}); // 2 elements of values 10, 20
auto vec2 = make_shared<vector<int>>(10,20); // 10 elements of value 20
Then again, I suppose you could also force copy/move construction if doSomeWork() used braces internally:
doSomeWork(v, decltype(v)(10,20)); // 10 elements of value 20
doSomeWork(v, 10, 20); // 2 elements of values 10, 20
But I still like that make_shared and make_unique use parentheses internally because you have to use parentheses as part of the function call anyway.
Great Scott I am a keen follower of your Effective series.
Let your book name be Effective ****
Post a Comment