Welcome back.

In this post, I continue to report on using clang-tidy to automate the modernization of the QuantLib code base.

Last time, I showed you a summary of the checks I had run so far, which I reproduce here for convenience. The files column contains the number of files touched, the lines column contains the number of lines touched, compiles shows whether or not the code compiled after the modernization, and fails PR merges shows if any of my sample set of pull requests (see this post) failed to merge against the modernized code.

  files   lines   compiles   fails PR merges
modernize-use-override 788   3302   no   11/43
modernize-use-auto 260   485   yes    
modernize-use-equals-default 182   237   yes   1/43
modernize-use-nullptr 149   243   no    
modernize-use-emplace 87   1724   yes    
modernize-use-default-member-init 46   126   yes   1/43
modernize-redundant-void-arg 4   5   yes    
modernize-use-equals-delete 3   4   no    
modernize-raw-string-literal 3   3   yes    
modernize-use-noexcept 2   3   yes    
modernize-deprecated-headers 2   2   yes    
modernize-replace-random-shuffle 1   20   yes    
               

I also described the reasons for the compilation errors reported in the third column. In one case, the reason was problems with boost::function, and after I published the post Daniel Duffy (thanks for the question, Daniel) asked if the same problems affect std::function: they don’t.

And now, I’ll continue posting my lab notes; this time, on the checks listed above that didn’t cause compilation problems. I won’t be showing a lot of examples of the changes performed; they’re described in the clang-tidy documentation, and in most cases they’re pretty straightforward. In a couple of cases, they’re so straightforward that I won’t comment on them.

Running the modernize-use-auto check didn’t change a lot of declaration, given the size of the code base; this is probably the reason why it didn’t cause any merge conflicts with existing pull requests. From what I see of the changes, it affected declarations which duplicated the type name, such as

    const IborCoupon* c = dynamic_cast<const IborCoupon*>(&coupon);

and declarations which involved iterators—even if a bit unevenly; I had one

    for (std::map<Date, Real>::const_iterator ii = quotes.begin(); // ...

turned into

    for (auto ii = quotes.begin(); // ...

but other similar cases remained unchanged.

If you belong to the Almost Always Auto camp (as I incline to), this might be a bit disappointing. Of course, I don’t suggest to go through the code and manually check all the other declarations where auto can be used; there are a whole lot of much better uses of one’s time. You can be a good scout instead, and clean up a bit every time you’re working on some piece of code. While you’re at it, you can also fix another side effect of the automated change: a declaration such as

     Leg::const_iterator cf = nextCashFlow(leg,
                                           includeSettlementDateFlows,
                                           settlementDate);

that we had lovingly aligned, will come out on the other side as

     auto cf = nextCashFlow(leg,
                                           includeSettlementDateFlows,
                                           settlementDate);

which irks me some. Here, using an automated formatting tool like clang-format would help; but on the one hand, we can’t enforce its use by all the contributors, and on the other hand, I’m probably getting softer and/or wiser in my old age and I no longer think it’s a big deal. I’ll hit a key and realign code like the above if I come across it, but I won’t go looking for it.

The figures for modernize-use-equals-default in the table above are for running the check alone on the code base. However, it should be run after modernize-use-default-member-init, since the latter can turn non-default constructors into default ones that can be converted. In fact, running the check afterwards increases the number of files touched from 182 to 199 and the lines changes from 237 to 256.

The modernize-use-emplace touched a lot of lines in the 87 files it touched; however, in a complete modernization of the code it would be less important than it seems. The reason is that a lot of those lines are in the test suite, and have to do with initialization of test data; that is, they belong to code like:

     std::vector<Foo> test_data;
     test_data.push_back(Foo(1));
     test_data.push_back(Foo(2));
     test_data.push_back(Foo(4));

The real modernization of the above wouldn’t be to replace push_back with emplace_back; it would be to sidestep both methods and write

     std::vector<Foo> test_data = { Foo(1), Foo(2), Foo(4) };

Unfortunately, moving to initialization lists will be manual work.

On a less important note, the replacement can turn the aligned

    discounters_.push_back(MarketModelDiscounter(cashFlowTimes[j],
                                                 rateTimes));

into the unaligned

    discounters_.emplace_back(cashFlowTimes[j],
                                                 rateTimes));

but I’ve already commented on this when talking of auto above.

The modernize-use-default-member-init check worked mostly as I expected, except for a few member initializers that it left unchanged; but my expectations might be at fault. Most of the cases are like

    class Foo {
      public:
        Foo() : x(Null<Real>()) {}
      private:
        Real x;
    };

which I expected to be transformed into

    class Foo {
      public:
        Foo() {}
      private:
        Real x = Null<Real>();
    };

Instead, the check has no effect on the code. The documentation does warn that only built-in types are affected; I’m not sure on which side of the fence the above falls, given that the data member is a Real which is a typedef to the built-in double type, but Null<Real> is a user-defined class that converts to Real.

The modernize-use-noexcept check only acted on a couple of lines; that’s because we don’t use exception specifications in our code base, so it didn’t find anything to convert. The one exception (no pun intended) was the destructor of our Error class, which inherits from the std::exception class and is used throughout the library as the type of the exceptions our code throws. We added a throw() specification to its destructor because it was required by one of the compilers we supported; the destructor of std::exception had the same specification in the implementation shipped with the compiler, and the latter would take exception (I’m on a roll tonight) if the destructor of our class didn’t.

In modern C++, however, noexcept specifications are a lot more important, since they allow to use move constructors (and thus to gain efficiency) in operations that require the string exception guarantee; see, for instance, this video for an example and more details. Unfortunately, adding them to the modernized code will be a manual operation.

Finally, a word on modernize-redundant-void-arg and modernize-deprecated-headers: they actually modernize the code to C++03, not C++11. Therefore, I backported their changes to the main code base; they were merged in pull requests #445 and #449, respectively.

That’s all for this post. I’m currently trying out the remaining checks on the library; I’ll report on the results in the next post in this series. As usual, I’ll be grateful for any feedback you’ll send my way; tweet or email me.

Subscribe to my Substack to receive my posts in your inbox, or follow me on Twitter or LinkedIn if you want to be notified of new posts, or subscribe via RSS if you’re the tech type: the buttons for all that are in the footer. Also, I’m available for training, both online and (when possible) on-site: visit my Training page for more information.