Maven on C++

Maven on C++
22/06/2021 Maven

Here at Maven, we are very excited about having upgraded our codebase to C++20 and the potential this brings for transforming the way that we write code.

So let’s survey how the landscape has changed post-modern C++ as we move into the C++20 era. C++20 brings some major new features to the language, what is now being called the big 4:

  • Concepts
  • Coroutines
  • Modules
  • Ranges

There are also a host of smaller features which greatly improve the code that can be written and that clean up existing features such as lambda expressions and structured binding.  We’re going to take a look at how these affect the code we write at Maven, so we won’t cover all of these, but for a good summary of the features of C++20 take a look here.

Current C++20 use at Maven

Our intention at Maven is to always live at head. By this, I mean we want to be working with the latest versions of the languages we work with but also, by extension, all of the tools we work with.  For example, we need the latest compilers to access the latest version of the language, and features such as Modules that require support from the underlying build system mean we must work with the latest tools such as CMake.  This is a philosophy that extends to all of the tools we use in our day to day work but also to the techniques that we apply in our development of the internal software libraries. A prime example of this would be Maven’s use of Concepts which we will explore to demonstrate how C++20 is changing our usage of the language.

Language support for concepts

The codebase at Maven heavily relies on templates. This has allowed Maven to create highly composable libraries, built for reusability and allows the compiler to create efficient code. However, on the flip side, this has added complexity. The Boost Concept Check library has been used to address complexity and has helped to produce informative errors but has added another library that developers must learn.

C++20 allows us to move away from Boost Concept Check. The migration is progressing, and to provide some insight as to how C++20 transforms our code we can look at our previous definition of the Trade concept.

#include <boost/concept_check.hpp>
#include <type_traits>
 
namespace maven::connectivity::concepts {
 
template<
    class T,
    class PriceType = typename T::PriceType,
    class VolumeType = typename T::VolumeType,
    class TimestampType = typename T::TimestampType>
struct Trade : Event<T>
{
    static_assert(std::is_same_v<PriceType, typename T::PriceType>, "Invalid PriceType");
    static_assert(std::is_same_v<VolumeType, typename T::VolumeType>, "Invalid VolumeType");
    static_assert(std::is_same_v<TimestampType, typename T::TimestampType>, "Invalid TimestampType");
 
    BOOST_CONCEPT_ASSERT((Price<PriceType>));
    BOOST_CONCEPT_ASSERT((Volume<VolumeType>));
    BOOST_CONCEPT_ASSERT((Timestamp<TimestampType>));
 
    BOOST_CONCEPT_USAGE(Trade)
    {
        MAVEN_CONNECTIVITY_CONCEPT_SAME_AS(PriceType) p [[maybe_unused]] = trade.getPrice();
        MAVEN_CONNECTIVITY_CONCEPT_SAME_AS(VolumeType) v [[maybe_unused]] = trade.getVolume();
        if (trade.hasAggressorSide())
            Side s [[maybe_unused]] = trade.getAggressorSide();
 
        MAVEN_CONNECTIVITY_CONCEPT_SAME_AS(TimestampType) t [[maybe_unused]] = trade.getTransactionTime();
 
        if constexpr (traits::HasMatchId<T>::value)
            if (trade.hasMatchId())
            {
                auto matchId [[maybe_unused]] = trade.getMatchId();
                BOOST_CONCEPT_ASSERT((MatchId<decltype(matchId)>));
            }
    }
 
private:
    Trade();
 
    T const trade;
};

Moved over to C++20 we can now rely on the language concept features.

#include <concepts>
 
namespace maven::connectivity::concepts {
 
namespace cxx20 {
 
template<class T>
concept Trade = Event<T> && requires(T const trade)
{
    typename T::PriceType;
    typename T::VolumeType;
    typename T::TimestampType;
    requires Price<typename T::PriceType>;
    requires Volume<typename T::VolumeType>;
    requires Timestamp<typename T::TimestampType>;
    { trade.getPrice() } -> std::same_as<typename T::PriceType>;
    { trade.getVolume() } -> std::same_as<typename T::VolumeType>;
    { trade.hasAggressorSide() } -> std::same_as<bool>;
    { trade.getAggressorSide() } -> std::same_as<Side>;
    { trade.getTransactionTime() } -> std::same_as<typename T::TimestampType>;
    // Optional requirements
    requires (traits::HasMatchId<T>::value ? requires
    {
        { trade.hasMatchId() } -> std::same_as<bool>;
        { trade.getMatchId() } -> MatchId;
    } : true);
    requires (traits::HasOrderId<T> ? requires
    {
        { trade.hasOrderId() } -> std::same_as<bool>;
        { trade.getOrderId() } -> std::same_as<OrderId>;
    } : true);
    requires (traits::HasTradeType<T> ? requires { { trade.getType() } -> TradeType; } : true);
};
 
}
 
template<cxx20::Trade> struct [[deprecated("use connectivity::concepts::cxx20::Trade")]] Trade {};
 
}

Concepts now supersede SFINAE

We have relied on SFINAE to support constraining functions in the past. For instance, a simple utility to decide if a message type is a member of a set of types was defined as follows:

template<class Type, class List> struct AnyOfType;
 
template<typename Type, template<class...> class List, class... Types> 
struct AnyOfType<Type, List<Types...>>
{
    static constexpr bool value = (std::same_as<Type, Types> || ...);
};

Given a function with the following signature that we might want to constrain to support only certain message types:

template<class Msg>
bool handle(DepthUpdate<Msg> const& update);

AnyTypeOf can be used to constrain the function either together with the return type

template<class Msg>
auto handle(DepthUpdate<Msg> const& update) 
-> std::enable_if_t<AnyOfType<Msg, SupportedMessages>::value, bool>;

or as a default argument

template<class Msg>
bool handle(DepthUpdate<Msg> const& update, 
std::enable_if_t<AnyOfType<Msg, SupportedMessages>::value> = 0);

Both obscure readability, and instead we can move the enable_if statement to the function template parameter list to improve this. Except now we’ve made a subtle error, because in C++ defaulted template parameters are not part of the signature, so multiple versions of the function will look like duplicate versions of the same function to the compiler, resulting in a compile error.

template<class Msg, typename = std::enable_if_t<AnyOfType<Msg, SupportedMessages>::value>>
bool handle(DepthUpdate<Msg> const& update);

However, it turns out that when dealing with non-type template parameters then default template parameters are part of the signature so we can work around this by changing the type of template parameter accepted.  This will now compile but as this is an integer value then we must consider that integers can legitimately occur in non-type template parameters.

template<class Msg, std::enable_if_t<AnyOfType<Msg, SupportedMessages>::value, int> = 0>
bool handle(DepthUpdate<Msg> const& update);

To guard against this we must change the signature one final time so that our non-type template parameter contains a void pointer.

template<class Msg, std::enable_if_t<AnyOfType<Msg, SupportedMessages>::value, void*> = nullptr>
bool handle(DepthUpdate<Msg> const& update);

Using this feature throughout the code base has been useful to constrain the contexts a function is callable in, but we increase the complexity and verbosity of our code to do so.  How do we teach all these rules to people new to the language or even remember them ourselves?  C++ 20 addresses this by allowing functions to be constrained by concepts directly. As you can see this does not affect the actual function signature, which is still readable.  Also now that our meta utility AnyOf is now a concept and not the static value member of AnyOfType structure.

template<typename Type, typename TypeList>
concept AnyOf = AnyOfType<Type, TypeList>::value;
 
template<class Msg>
requires meta::AnyOf<Msg, SupportedMessages>
void handle(DepthUpdate<Msg> const& update);

Requires clauses with if constexpr

As much of the codebase is templates, there are occasionally locations in the code where the behaviour may diverge depending on a trait of the underlying template type. C++17 made handling these situations much easier with the introduction of constexpr if statements.  Historically we’ve relied upon the Boost Hana library to help in such situations:

constexpr auto hasEpochTime = boost::hana::is_valid([](auto type) -> decltype(std::declval<typename decltype(type)::type>().getEpochTime()) { });
if constexpr(hasEpochTime(boost::hana::type_c<std::decay_t<decltype(msg)>>))
{
    setSeconds(std::chrono::seconds(msg.getEpochTime()));
}

In C++20 we can rely directly on language features to achieve the same effect in a much more readable and maintainable fashion.

if constexpr( requires { msg.getEpochTime(); } )
{
    setSeconds(std::chrono::seconds(msg.getEpochTime()));
}

Abbreviated function templates & constrained auto functions

It was C++14 that gave us the ability to create generic lambda functions, and for the first time allowed the auto keyword to be used to declare types in the lambda parameter list.  C++20 extended this feature to template functions, allowing much of the boilerplate of templates to be removed.  For example, given a template function:

/// Takes a list of order details and modifies all orders by the specified quantity.
/// \param[in] existingOrders Map of existing order info.
/// \param[in] modifyPriceBy Price amount to reduce existing order by.
/// \param[in] modifyVolumeBy Volume quantity to reduce existing order by.
/// \tparam O Container of orders.
/// \tparam P The price type for orders.
/// \tparam V The Volume type for orders.
/// \return The modified copy of the original input map modified by the specified amount.
template<typename Orders, typename Price, typename Volume>
auto modifyExistingOrders(
    Orders const& existingOrders, 
    Price const modifyPriceBy, 
    Volume const modifyVolumeBy
)
{
    Orders orders;
    std::transform(existingOrders.cbegin(), existingOrders.cend(),
                   std::inserter(orders, orders.end()),
        [modifyPriceBy, modifyVolumeBy](auto i)
        {
            std::get<1>(i.second) += modifyPriceBy;
            std::get<2>(i.second) += modifyVolumeBy;
            return i; 
        }
    );
    return orders;
}

We can now simplify the function declaration by making the function parameters auto. Additionally, we know a little more about the expected behaviour of a few of the parameters so we can choose to constrain them using our concepts. This has the added benefit of making our code self documenting. The Doxygen tool has been threatening to support concepts for a while, and when support is complete, this should have the added benefit of auto documenting requirements upon the template parameters. Note however as is often the case with auto parameters that we must now use the decltype keyword within the function when we require type information of auto parameters to declare a container of orders internally.

/// Takes a list of order details and modifies all orders by the specified quantity.
/// \param[in] existingOrders Map of existing order info.
/// \param[in] modifyPriceBy Price amount to reduce existing order by.
/// \param[in] modifyVolumeBy Volume quantity to reduce existing order by.
/// \return The modified copy of the original input map modified by the specified amount.
auto modifyExistingOrders(
    auto const& existingOrders, 
    concepts::cxx20::Price auto const modifyPriceBy, 
    concepts::cxx20::Volume auto const modifyVolumeBy
)
{
    std::remove_cvref_t<decltype(existingOrders)> orders;
    std::transform(existingOrders.cbegin(), existingOrders.cend(), 
                   std::inserter(orders, orders.end()),
        [modifyPriceBy, modifyVolumeBy](auto i)
        {
            std::get<1>(i.second) += modifyPriceBy;
            std::get<2>(i.second) += modifyVolumeBy;
            return i; 
        }
    );
    return orders;

Future C++20 Usage

These examples are just a taste of the way C++20 is beginning to transform the way we write code at Maven. There is lots more for us to do with C++20 yet, and to some extent, we are constrained by compiler support or by where we can bridge the gap between varying levels of compiler support for different features.

A prime example here would be the situation with Ranges where Clang 12 is currently lagging on support within its standard library implementation, libc++, compared with GCC 10/11 and the companion standard library libstdc++. We have at times previously built against libstdc++ under Clang while we’ve waited for its standard library support to catch up. We may do so again, or we have considered the use of external Ranges implementations such as Eric Niebler’s Range v3 library as a bridging solution. Currently, some of our applications that are only built under GCC are beginning to use them. Either way, Ranges is likely to be the next feature included at Maven.

Coroutines are another large addition that brings new control flow constructs to the core of the language. This is a powerful new set of tools for addressing some of the obstacles faced in asynchronous code. Many of our trading system components are written asynchronously, particularly for client and server components. Yet they currently represent an area of the codebase where we currently abandon one of C++ strongest features, the strict nesting of scopes and object lifetimes. Eric Niebler has talked about just this issue in his post on structured concurrency. This presents us with an opportunity to transition from a model where each component must manage a state machine, to where we can rely on the native control flow constructs of the language to help us write idiomatic modern C++ which will do much of the heavy lifting for us.

Finally, modules present a mechanism to improve compile-time performance and provide a better separation between interface and implementation of a library. It might be reasonable to expect features like concepts to improve compiler performance compared to emulating constraints via concepts such as SFINAE. Odin Homle points out in his talk “Type Based Template Metaprogramming is Not Dead” using SFINAE is about the most expensive operation a compiler must perform for templates because to build the overload set for a function the compiler must instantiate all of the functions that can be part of the overload set.

But the very act of constraining template does add extra work that the compiler must do. Herb Sutter discussed the effect that initial attempts to apply concepts to the standard library had on compile-time performance, this was an issue the committee faced in the design of concepts. Work is currently ongoing within the C++ Standards Committee looking at how to modularise the C++ standard library which should help to mitigate the cost of adding additional constraints.

At Maven we are keen to leverage modules in our libraries, but the current landscape across compilers is one of fragmented support. Modules also require support from the build system, and while there is currently experimental support within CMake, work here is yet to be finalised.

Summary

Migrating a large codebase to a new version of the language is a challenging proposition. Yet doing so offers many long term rewards.  We now have a new set of tools to address some of the obstacles that we face in our codebase using idiomatic modern C++20. This provides the opportunity to simplify many instances of the complex usage of the language which have raised barriers to entry, and in doing so eases the maintenance burden for future development efforts.  This is something that matters to us at Maven because this is the development equivalent of investing in the long term future of all technology projects, which is the lifeblood of what we do at Maven.

We hope to share more of our experiences with the language with you in the future. In the interim, if any of this has piqued your interest and you’d like to know more, then feel free to ask us anything.