You can migrate gradually, though cleaning up the boundaries later can be tricky.
Please look at the C++ Core Guidelines before rewriting old code, because they provide lots of very sensible guidance. For example:
Quite often, using raw pointers is still appropriate, in particular to indicate borrowed/temporary ownership, except in function arguments where you'd use a reference.
I'd therefore suggest that you migrate one function and file at a time, modernizing the internals and the interfaces. Fix up the call sites to those functions as you go to translate to and from smart pointers. For example, if you update a function T* create()
to unique_ptr<T> create()
, you'd update the call site from create()
to create().release()
until you're modernizing that part as well.
Unfortunately this won't work very well for shared ownership because a shared_ptr
would be responsible for deletion, but ideally that kind of tangled ownership would be rare anyway. You will have to update all co-owners in one go. If in doubt, leave those complicated parts for later and first attack those functions where smart pointers bring valuable clarity without larger risks.
Of course general refactoring best practices apply, e.g.:
- have some tests before you change the code
- even a very superficial test helps a lot, don't even make detailed assertions, just aim for >80% line coverage
- make small changes, commit early and often, keep the entire project in a working state
- I have messed up way too many refactoring efforts by not respecting this rule. Doubling the effort of a refactoring is cheap when it means you're not stalling other important work.
- use temporary compatibility shims to keep the code runnable.
- E.g. instead of changing
T* create()
to unique_ptr<T> create()
directly:
- start by creating a compatibility layer like
unique_ptr<T> create2(); T* create() { return create2().release(); }
,
- then gradually move all call sites over to
create2()
,
- then mass-rename
create2
→create
.
A large “risk” in this effort will be that you will encounter memory safety issues, e.g. use-after-frees. If so, it will likely be easiest to fix it immediately. This ties in with an incremental refactoring approach, because that will probably allow you to make maintenance releases before you've completed going through all 2k files.
Given the large scope of your refactoring, it may not be sensible to move to smart pointers entirely. Only do this where it brings substantial value. The value of smart pointers is that they make it easier to write correct code. Thus, focus your efforts on components that are likely to be modified in the future. In contrast, you should leave code unchanged (for now) if it has been barely touched in a decade.