Software development

Taking It Slow: Managing Code Deletion in C++

Introduction

In this blog, the ArcGIS Native Maps SDK for Qt engineering team shares their journey in managing legacy code deletion within their large and mature C++ codebase.

Within the Qt Maps SDK, we have a layer of generated C++ code that is tightly integrated into our codebase and continuous integration systems. This code, while lightweight and minimally impactful on our builds, runtime performance, and memory footprint, has gradually become redundant over time.

Although this redundancy is not a significant detriment, it does incur costs. The tools required to generate this code must be maintained, the generation process consumes time, and the generated code needs to be archived. Additionally, this contributes to increased compile times.

Recognizing these inefficiencies, we identified opportunities to enhance our workflow. Effective communication was crucial in motivating the problem and involving the team and key stakeholders in the process.

Throughout this blog, we outline the drawbacks of maintaining this technical debt and the benefits of addressing it. We describe the iterative approach we adopted to manage this issue over time, ensuring stability for our users. By breaking down the problem into manageable chunks, we provide an achievable means for reducing legacy code over time that doesn’t impact the end user product.

Navigating the challenges of code removal

To understand the scale of legacy code deletion required, we identified roughly 750 generated C++ classes, the number of which continues to grow as we expand our products. These generated classes provide a robust Qt abstraction over a C API. Given that our current codebase is built atop this abstraction, it’s not a simple task to just remove it. Additionally, we have existing codebase generation processes that assume the use of this Qt abstraction, which will also need to be updated.

Code examples

Let’s take a look at some examples. It’s essential to understand the structure of our codebase, which employs the pimpl idiom (see the code below). Below, we go through some examples of these internal, pimpl classes.

class MapImpl {
 std::unique_ptr<OldMapAbstraction> m_impl;

public:
 explicit MapImpl(std::unique_ptr<OldMapAbstraction>&& impl);
 const std::unique_ptr<OldMapAbstraction>& getOldAbstraction() const;
};

The goal of this exercise is to remove OldMapAbstraction from the codebase. Here is how OldMapAbstraction is structured. We want to use its underlying C API object directly in MapImpl.

class OldMapAbstraction : public QObject{
 void* c_api_handle = nullptr; // ownership semantics not covered here. Simplified for explanation.

public:
 explicit OldMapAbstraction(void* c_api_handle);
 void* getCAPIObject() const;
};

This creates a problem if we simply swap out OldMapAbstraction with its underlying C API representation directly. For example, here is what that would look like:

class MapImpl {
 // the C API object. Here 'NewMapAbstraction' is the strongly-typed C API object
 std::unique_ptr<NewMapAbstraction, CustomDeleter> m_uniqueHandle;

public:
 explicit MapImpl(std::unique_ptr<NewMapAbstraction, CustomDeleter>&& impl);
 const std::unique_ptr<NewMapAbstraction, CustomDeleter>& getNewAbstraction() const;
};

It should be apparent now that any other class in the codebase that deals with MapImpl must now change as well. Changing one class ripples through the entire codebase, necessitating more and more changes and eventually turning the task into an all-or-nothing problem.

Implementing the solution

To address this challenge, our team designed a solution based on the adapter pattern. We began by prototyping a few classes, ensuring that each class could be consumed with either the generated Qt abstraction layer or directly with the underlying C API. This approach was crucial to allow the codebase to be adapted incrementally without disrupting existing functionality.

Here is an updated version of our MapImpl class:

class MapImpl {
 // the C API object. Here 'NewMapAbstraction' is the strongly-typed C API object
 std::unique_ptr<NewMapAbstraction, CustomDeleter> m_uniqueHandle;

public:
 // can still be constructed from the 'old' abstraction. This allows calling code
 // to remain unchanged. This ctor will convert the old abstraction to the new one.
 explicit MapImpl(std::unique_ptr<OldMapAbstraction>&& impl);

 // the new abstraction can be used directly as well
 explicit MapImpl(std::unique_ptr<NewMapAbstraction, CustomDeleter>&& impl);

 // we still provide a way to get back the old abstraction so calling code can be changed incrementally
 const std::unique_ptr<OldMapAbstraction>& getOldAbstraction() const;

 // and the new one, too
 const std::unique_ptr<NewMapAbstraction, CustomDeleter>& getNewAbstraction() const;
};

How we aligned with stakeholder needs

This is how we approached it:

  • Identifying work and engaging stakeholders:
    • After identifying the necessary work for code deletion, the next step was to get stakeholders, such as product owners, on board with implementing the work.
    • As a product team, we already have a backlog of features, bug fixes, and enhancements to address in each release.
  • Integrating work into planning:
    • Requesting a substantial block of time solely to implement these changes is unfeasible.
    • We integrated the work into our release and sprint planning, scheduling it in manageable pieces.
  • Efficiently addressing technical debt:
    • This approach allows us to address technical debt while meeting ongoing development commitments.
    • It is efficient for both stakeholders and engineers, helping us eliminate redundant code without disrupting the development cycle.

Putting it into practice

Following stakeholder engagement and approval, we have been rolling out this new pattern throughout our codebase for over a year now. We’ve already seen major benefits to our approach, including being able to move at our own pace, and not risk slipping on priority features for our users. We can also migrate single classes without risking that cascading throughout the codebase.

Whenever we implement new code, it’s always written in the new pattern. This ensures that the scope of classes to migrate doesn’t increase.

Benefits

Over time we will be able to streamline our codebase by pruning out parts of the generated code, which will lead to improved build times. As we remove the generated abstraction layer, the memory footprint of user apps will be improved. Additionally, once the process is complete, we can simplify our builds by eliminating the need for the generated code altogether. While these improvements may be incremental, they are valuable wins from both a user and an engineering team standpoint.

As an engineering team, we are already seeing significant advantages. This method allows us to introduce new systems gradually, ensuring they are thoroughly tested without compromising our commitment to quality. A concrete example of this is our async task system. We have implemented a new version that all classes following our new pattern opt into. This enables us to roll out changes incrementally without needing to switch over our entire API to the new system. We are already observing key benefits from our new implementation and can make improvements over our old system without risking any destabilization.

Lessons learned

As with many things, we have found that communication is key. Motivating the problem, engaging with and involving the engineering team and key stakeholders in the process is important. Based on our experiences, we found the following pattern worked well for achieving our goals:

  • Outline the detriments of living with the technical debt you’re tackling, along with the benefits the team will reap by addressing it.
  • Get buy-in along the way. Work with the team to identify the new patterns and put them into practice.
  • Devise an iterative approach to tackle the problem over time. This step is crucial.
  • When time permits, schedule small tasks of migration. With the patterns solidified, this is no longer R&D; it’s simply code migration.

In conclusion

Managing code deletion in a large and mature C++ codebase is a complex but beneficial endeavor. By adopting an incremental approach, we have been able to streamline our codebase, improve build times, and the memory footprint of user applications. This method has also allowed us to introduce new systems gradually, ensuring thorough testing and maintaining our commitment to quality.

We hope our journey provides valuable insights and practical strategies for your own codebase management. We invite you to share your experiences and challenges with us, as we believe that collective knowledge and collaboration can drive continuous improvement in software engineering.

If you’re passionate about tackling technical challenges and want to be part of a dynamic team, we encourage you to explore career opportunities at Esri. Visit our careers page to learn more about how you can join us in shaping the future of geospatial technology.

About the author

James Ballard

James has been with Esri for more than 15 years, focused on helping developers solve tough GIS challenges with powerful SDKs and APIs. He leads the ArcGIS Maps SDK for Qt team and is passionate about building tools that help users create effective, real-world solutions.

Next Article

Positions of GPS Satellites in 3D

Read this article