Large Code Bases
Large Code Bases
Andrew Willmott
What is Large?
• Several million lines of C++
• Generally:
• Graphics Engine
• Game code
Why is this different?
• Scale massively affects Software Engineering issues
• Complexity management
• Iteration times
• What can make sense at a small scale may be completely unworkable at a large
scale
Large Codebase Concerns
• Understandability
• Compile times
• Debuggability
API Understandability
• Goal: should be able to easily find and understand any needed API
• Common Issues
• Implementation in header: harder to pick out the bits that you care about
• Badly formatted, or big ball of mud: hard to find what you need quickly
Recompile Times
• How long does it take to recompile updated code
• Language features are primarily added via complex templated library header
files
• Inheritance
• Global declarations
• Public header should only include those things clients care about
• Compounding positives:
• Fewer includes
• Where not possible, consider fixing the issue, whether it's a class-scoped
variable or inability to split out a needed type.
• class BLAH;
• Interfaces
• PIMPL
virtual void add_time( const F32 elapsed_time ) = 0; //!< Add given +ve or -ve time delta to the current animatio
virtual void set_time( const F32 time ) = 0; //!< Jump directly to the given time
virtual void reset() = 0; //!< We want to restore this node back to sensible defaults
virtual bool process() = 0; //!< Updates output according to current state. Should first
virtual const ANIM_OUTPUT& get_output() const = 0; //!< Returns output from last process() call
virtual void get_parameters (SICORE::JSON_VALUE* params) const = 0; //!< Returns JSON version of this process's cu
virtual void update_from_parameters(const SICORE::JSON_VALUE& params) = 0; //!< Update internal state from the given set
};
Private Header
class ANIM_MIRROR : public I_ANIM_PROCESS
{
public:
// I_ANIM_PROCESS interface
void* as_class(SICORE::IDENTIFIER class_id) const override;
// ANIM_MIRROR methods
void set_active(bool enabled); //!< Can be used to toggle mirroring on and off
bool is_active() const;
protected:
SIMATH::QUAT get_parent_rotation(int node_index); //!< Find modelspace orientation of parent
I_ANIM_PROCESS_REF m_input;
SKELETON_CONST_REF m_skeleton;
U32 m_input_id_hash = 0;
…
More on Interfaces
• Generally idea is to use this only for major manager or subsystem classes.
• Widely used in both OS APIs (including DirectX) and shipped games over
many years.
• All implementation detail goes into an internal class that is only forward
declared in the header
• Drawbacks:
class UI_FILE_BROWSER
{
public:
PIMPL_DECL(UI_FILE_BROWSER);
void set_location( const SICORE::FILE_PATH& path ); //!< Set starting location of browser
void set_location( const SICORE::FILE_ITEM* item ); //!< Set starting location of browser
void set_file_type ( const SICORE::FILE_TYPE& file_type ); //!< Set the allowed file type.
void set_file_types( S32 num_types, const SICORE::FILE_TYPE types[] ); //!< Set the allowed file types. This may include FIL
void set_show_filtered_items(bool enabled); //!< Sets whether to still show unallowed items as inactive, rather tha
void set_close_on_selection (bool enabled); //!< Sets whether the dialog auto-closes on item selection
void set_sort_mode( SICORE::FILES_SORT_MODE mode ); //!< Set sort mode for container contents
bool show( const C8* label, bool* opened ); //!< Show file browser window. Returns true if the user selects a file (
SICORE::FILE_ITEM_REF get_selected_item() const; //!< Returns selected file (or container if that was included in the all
SICORE::FILE_ITEM_CONST_REF get_location() const; //!< Returns the location we initially set.
};
Source or Private Header
struct SIAPPLICATION::UI_FILE_BROWSER::PRIVATE
{
SISTL::vector<FILE_ITEM_REF> m_pane_items; //!< Left-to-right list of containers for each pane
SISTL::vector<FILE_ITEMS_REF> m_pane_contents; //!< Corresponding container contents
bool m_show_filtered = true; //!< If true, files that aren't filtered are still shown, but as inactive selections.
bool m_close_on_selection = true;
bool show(); //!< Show dialog internals. Returns true if a valid selection was double-clicked
void refresh_panes( int start_pane, int num_panes ); //!< Fetch all the file/container items in the specified panes
bool is_supported(const FILE_TYPE& type) const; //!< Returns true if given file type is supported
};
class ENTITY;
ENTITY* create_entity();
void manipulate_entity(ENTITY* entity);
int get_entity_value(ENTITY* entity);
void destroy_entity(ENTITY* entity);
• Initial extra effort in setup easily pays for itself in the long run
• Don’t mix public and private, and always put public at the top
• Hides implementation
• In particular, can’t easily change base functionality, as you don’t have a clear
picture of how it’s being used
More Suggestions
• Prefer explicit over implicit code. Clever systems that automagically register classes
or entities obfuscate code. A manual call hierarchy is easier to debug + introspect.
• Don't use "advanced" C++ features. Avoid using new C++1x features unless already
proven, i.e., shown to be practical. (Reduce language complexity/surface area.)
• Keep argument lists small. If they start getting unwieldy, consider creating some form
of FUNC_INFO struct that is passed in instead.
• Use pointers rather than non-const references for arguments that will be modified.
(Makes argument use obvious from call site, without looking at implementation.)
The Golden Rule
• KISS