logo

Book: Effective Modern C++

Posted on: 2015-02-12

Summary

'Effective Modern C++' by Scott Meyers is the latest release in the 'Effective' series for C++. As the rest of the series, it covers the subject matter into readable 'Items' that explore a subject.

This book is good in that it covers many of the salient points of the C++11 and C++14 standard in an accessible and deep way. It also contains a lot of real world experience of using the features something you are unlikely to have in a reference volume.

Whilst it is a good book - it's perhaps not as strong as the others in the series. The book might be improved with a chapter laying the groundwork of what the features of the new standard and also describing what some of the more nebulous concepts are. C++11/14 is significantly more complicated than C++98. This leads to much of the book describing work around and hacks for ever more gnarly edge cases. This is frankly worrying for the long term viability of the language.

Discussion

I recently read the book 'Effective Modern C++'. I'm generally a big fan on the books in the 'Effective' series - and have C++, C#, Objective C and JavaScript versions too.

Most C++ I develop is for the rather prehistoric C++98 largely for compatibility reasons. My motivation was to see what implications were for the C++11 and C++14 standards which I hadn't had much exposure to.

One of the nice properties of the Effective books is the way they work through a wide variety of issues step by step, explaining why certain decisions were made. In many programming scenarios there isn't just one way to do something, even if there are solutions which are generally better. Moreover understanding why a design choice was made allows you to choose whether that choice applies to your particular scenario.

To cut to the chase I found the experience a little disturbing. Firstly I don't think this book is as good as the other Effective C++ books, and secondly I'm finding where C++ is going is a rather difficult place.

C++ has always been a complicated language. It's gone through many changes, each time with new features and rules. I've had an ongoing love-hate relationship with it. Overall despite its problems, it's my language of choice. That said I can't help feeling, with the long lists of rules and exceptions explored in Effective Modern C++ that the complexity is getting a bit out of hand.

As usual the majority of the Items are interesting, and discussed in an engaging manner. The first problem is that one of C++11s big feature is the addition of move semantics. In order to understand how move semantics work you need to understand what lvalue and rvalue are. The book only really touches on this in the introduction

"A useful heuristic to determine whether an expression is an lvalue is to ask if you can take it's address. If you can it typically is. If you can't, it's usually an rvalue. A nice feature of this heuristic is that it helps you remember that the type of an expression is independent of whether the expression is an lvalue or a rvalue."

That's helpful but doesn't really explain what they are, or why they are important. Without that it's kind of hard to to know why it's so important for move semantics. Which makes some of the subsequent discussions somewhat confusing.

Since C++11/14 is significantly more complicated than previous versions many of the Items go through the nuances and edge cases of the new features. It happened a few times where on reading through an Item I decided I just wouldn't use the feature, as the issues outweighed the benefits. Having to explain all the complexity also makes some of the Items long lists of complicated and somewhat dull side effects. As programmers we want to code and get things done. We want our brains to mainly hold the brilliant new algorithms we want to implement not the details and intricacies of some fudged C++ feature. I think the author gives a good shot at trying to make it accessible, and practical but it just falls down. Sometimes because the authors conclusion doesn't convince, and other times the hurdles required grind it's use out of you.A short discussion follows of some of the items in the book.

"Item 6: Use the explicitly typed initializer idiom when auto deduces undesired types."

Seems a little weak. The previous Item talked about preferring to use auto instead of specifying a type. This item points out for some types this doesn't work because the thing returned is a proxy to the type. It's suggestion though is to still use auto and add a static cast

auto highPriority = static_cast<bool>(features(w)[5]);

I don't see how that's superior in any way than doing

bool highPriority = features(w)[5];

"Item 9: Prefer alias declarations to typedefs"

Makes a reasonable argument to use using the 'using type = ' style but only worth doing, maybe, if you've already committed to C++11/14. Typedefs in C++98 aren't much different.

"Item 10: Prefer scoped enums to unscoped enums"

You can simulate scoped enums in C++98, by wrapping the enum in a class. You don't get being able to forward declare and specify a backing type - both nice features.

The other problem with the C++98 style is you can't specify the type directly. In effect you have

struct Thing 
{
    enum Enum 
    {
        BLOCK,
        TRAIN,
        WALL
    };
};

Thing::Enum thing = Thing::WALL;

Having to write Thing::Enum is a little ugly, as well as the extra boiler plate isn't much fun either. In practice I have macros that set the majority of this stuff up. Also since I need reflection to have the enum names - I have extra macros that generate a map from values to strings. Doing the C++98 style in some respects simplifies things, because the reflection data can belong to the struct, and then the contained enums can be referenced via unqualified name.

So for my uses the advantage is purely that a backing type can be specified and because the enum is wrapped in a struct it still cannot be predeclared. So I will still use wrapped style even with a C++11 compiler.

Smart Pointers Chapter

I don't generally use the standard template library (STL). It's not all bad, and it's great it already exists and works. That said I think it's architects are way too enamoured with templates. I'm generally of the mind that templates should only be used if really needed - say for containers, smart pointers, type reflection, type conversions, some simple algorithms and perhaps not much else.

Another facet of the STL creators philosophy is to be able to use STL facilities without having to modify any pre-existing code. A laudable goal - but produces some nasty solutions. In particular std::unique_ptr and std::shared_ptr. Both have the unpleasant feature of once you have one, you can only pass around as a smart pointer of the appropriate type. If you don't all bets are off. That's because the thing being pointed to has no knowledge it is being referenced by a smart pointer, and having some independent way to look up if there was a smart pointer would be too slow. std::shared_ptr has to actually allocate an independent chunk of memory to do the tracking of an objects life time through reference counting. There is a workaround for the extra allocation actually discussed in the book but that's yet another hack.

std::unique_ptr also has extra bells and whistles such that you can give it a function to call to destroy the object.

So how to avoid these problems? If you relax the requirement that it work with any class, you can have a base class that holds the per object state information. You can have different base classes and smart pointers for different life-time strategies or other features. Also you can use a virtual function for destruction - for some control on how destruction occurs. Doing it this way means simpler classes, smart pointers, and no separate allocation needed. You can also pass a naked pointers if you want to without problems.

So for me this chapter was interesting as it solidified my view that using STL smart pointers should be avoided if you can.

Item 27: Familiarize yourself with alternatives to overloading on universal references.

The main result for me of this chapter was that using universal references with overloading probably isn't a good idea. The proposed solution is frighteningly ugly. It's nice that you can make it work - but I don't think that's a path I'll be taking in production code.

Item 29: Assume that move operations are not present, not cheap and not used.

Well that's a bit of an eye opener. It begs the question why does C++ have move semantics? It's a very C++ type solution, to a problem that other languages don't seem to have.

What's the problem? Perhaps an example...

String doSomething(int a) 
{
    String s;
    s.concatInt(a);
    return s;
}
String choose(int which, const String& a, const String& b) 
{
     return which == 0 ? a : b; 
} 
String r(choose(0, doSomething(10), doSomething(20)));

In C++ if you return something that has scope it's generally wrapped in a class that will destroy it when it's no longer used.In the example String is a class which keeps the contained characters in scope. In a simple implementation of String it mallocs the contents, and frees when the destructor is called. The copy constructor allocates a whole new contents and so forth. You can of course do better with this with ref counting and the like - but the point is for the result to be kept in scope there is work. In the example above if there isn't the return value optimization there will be 3 copies that perhaps weren't needed.

That's 3 pointless allocations and frees. With move semantics, if the compiler can determine that the rvalue is no longer accessible, instead of copying the contents can be 'moved' perhaps allowing for less work. In the String example the move can just grab the allocated contents, and set the rvalue to a value that the dtor knows it won't have to free.

It's an optimization that aims to remove some of the cost of the C++ way of keeping things in scope. Other languages generally don't generally have the problem - for example in garbage collecting languages, you can pass back a pointer, and it will be freed through the collector at some later stage. There is no need for this active 'keeping the value alive'.

Equally in C++ if your the cost of keeping things in scope is cheap (say with reference counting or some other mechanism) move semantics may not buy you much.

Later the book Item "41: Consider pass by value for copyable parameters that are cheap to move and always copied", is somewhat at odds with Item 29. To his credit the author knows this and writes about the issues.

Because of this and other issues (such as the extra complexity of making types have move) I've limited the places to where I implement it - on smart pointers, often used containers and strings. That's it. I ignore item 41.