0% found this document useful (0 votes)
11 views31 pages

Large Code Bases

Developing for bugger code base

Uploaded by

alyxredmond
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
11 views31 pages

Large Code Bases

Developing for bugger code base

Uploaded by

alyxredmond
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 31

Coding for Large Code Bases

Andrew Willmott
What is Large?
• Several million lines of C++

• Common for any AAA game these days

• Generally:

• Large shared codebase providing core functionality

• Graphics Engine

• Game code
Why is this different?
• Scale massively affects Software Engineering issues

• Complexity management

• Working within a large team

• Iteration times

• Most C++ references and textbooks are targeted at ~1000-100,000 line


codebases.

• What can make sense at a small scale may be completely unworkable at a large
scale
Large Codebase Concerns
• Understandability

• Majority of code is code you didn’t write

• Learning the codebase, making changes easily

• Compile times

• Single-line-change iteration time, recompile times, *relink* times.

• Debuggability
API Understandability
• Goal: should be able to easily find and understand any needed API

• Common Issues

• More files: need to skip around a lot to get complete picture

• 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

• How long does it take to recompile affected code

• That one header everything includes

• Implementation leaking into interface


Compile + Link Times
• Number of files opened while compiling a module

• Number of lines parsed while compiling a module

• Note: Cost of file open much bigger than the read

• Number of redundant symbols that must be unified or stripped by the linker

• Sheer symbol count


Causes?
C++
• C++ is broken by design for large codebases

• A class definition must include implementation!

• Still waiting on module support decades later

• But hey we have <wat> from c++1<x>

• Language features are primarily added via complex templated library header
files

• But, almost all large code bases are written in C++


Implementation Detail in Headers
• Changing implementation requires recompile of all client code

• Mixture of public and private makes API less intelligible

• Leads to extending the size of the compilation unit

• Inheritance

• #including other files for implementation detail


Compilation Unit Bloat
• Implicit instantiation: templates

• Implementation repeated in each including module, redundancy removed by


the linker

• Contrast with explicit IntArray class: single implementation in single module

• Global declarations

• Can wind up repeated in all including modules unless care is taken

• constexpr helps these days


Dead Code
• Code that isn’t called by anything

• Code within functional code that is never exercised (future looking?)

• Often kept as a “safety blanket”

• Makes it difficult to reason about how code can be fixed or extended

• Makes it difficult to refactor or clean up code

• Deleting more lines than you add is productive in a large codebase!


A Fix: Implementation Hiding
• Biggest thing: focus on cleanly separating interface (API) and implementation.

• Public header should only include those things clients care about

• Changing implementation should only cause local recompilation

• Compounding positives:

• Fewer includes

• Those includes are smaller

• Fewer public symbols


Stage 1: Forward Declaration
• You should all have heard of this

• Forward declare everything where possible

• Where not possible, consider fixing the issue, whether it's a class-scoped
variable or inability to split out a needed type.

• class BLAH;

• enum BLAH : S32;

• class BLAH { enum COW : S32; class BOB; };


Some Implications
• Always declare storage type for enum. (Doesn’t have to be enum class)

• Don’t use class scope. (Can’t forward declare)

• No nested enums, classes/structs

• Prefer functions if there is no state

• Prefer functions over static methods

• Don’t #include platform/OS headers from public API


Stage 2: Hide the rest
• If you're adding anything that will be commonly used, you need to go beyond
this and also hide as much of the implementation from clients as possible.

• Three possible methods:

• Interfaces

• PIMPL

• Functional APIs (C-style).


Interfaces
• “Abstract Base Class” — all methods pure virtual, no data.

• “Concrete” implementation class inherits from interface, header is private

• Pros: plugin nature, DLLs

• Cons: virtual function call overhead, separate (third) header


Public Header
class I_ANIM_PROCESS : public REF_COUNTED
{
public:
virtual void* as_class(SICORE::IDENTIFIER class_id) const = 0; //!< Returns given class interface if supported, or nullptr i

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 S32 get_num_inputs() const = 0; //!< Returns current number of inputs.


virtual void set_input(S32 index, I_ANIM_PROCESS* input ) = 0; //!< Sets the given process input. Unhandled proce
virtual I_ANIM_PROCESS* get_input(S32 index) const = 0; //!< Returns the given process input, or 0 if unset.
virtual void clear_inputs() = 0; //!< Remove all current inputs

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;

void add_time(const F32 elapsed_time) override;


void set_time(const F32 elapsed_time) override;
void reset() override;

bool process() override;

S32 get_num_inputs() const override;


void set_input( S32 index, I_ANIM_PROCESS* input ) override;
I_ANIM_PROCESS* get_input(S32 index) const override;
void clear_inputs() 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.

• Often combined with intrusive reference counting for lifetime management.


PIMPL
• “Private IMPLementation”

• All implementation detail goes into an internal class that is only forward
declared in the header

• blah.h/blah_internal.h/blah.cpp, or simply blah.h/blah.cpp

• Drawbacks:

• Two allocations per class instantiation (can be avoided with wrappers)

• Need either pass-through functions or prefixing internal data references


Public Header
#include "sicore/generic/patterns/pimpl.h"

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

FILE_ITEM_REF m_selection; //!< Currently selected item


FILES_SORT_MODE m_sort_mode = FILES_SORT_MODE::SORT_NAME_ASCENDING_CASE_INSENSITIVE;
SISTL::vector<FILE_TYPE> m_file_types; //!< File types to filter against, if any

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
};

void UI_FILE_BROWSER::set_file_types( S32 num_file_types, const SICORE::FILE_TYPE file_types[] )


{
_.m_file_types.assign(file_types, file_types + num_file_types);
}
Functional API
• Convert the class into data (ideally with a substantial part hidden) and a set of
functions that operate on that data.

class ENTITY;

ENTITY* create_entity();
void manipulate_entity(ENTITY* entity);
int get_entity_value(ENTITY* entity);
void destroy_entity(ENTITY* entity);

• Often use handles rather than pointers


Other Fixes
• Highly recommend using at least one of these approaches

• Larger codebases often use several

• Initial extra effort in setup easily pays for itself in the long run

• However, lots more you can do…


Understandability
• Keep all public methods/functions together, as neatly laid out as possible

• Don’t use inline method declarations

• Don’t mix public and private, and always put public at the top

• Break up into functional groups

• Comment every method with //<! what this does


Be Kind to the Linker
• Put everything possible in the anonymous namespace

• Hides implementation

• But also means the linker can discard those symbols

• Avoid inline templated code

• Either avoid templates, or consider explicit instantiation


Avoid Concrete Inheritance
• One of the biggest failings of OOO

• Threads the implementation of a system through several abstraction layers &


files

• Must understand entire system to change anything!

• 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.

• Don't use in-class method definitions. (Clutters API.)

• 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

• Everything else is noise

• Feature coding is all about managing complexity.


Conclusion
• These may seem like niceties, but being strict about API vs implementation is
key to:

• Getting compile times down below five minutes

• Allowing new coders to get up to speed quickly

• Increasing code productivity in general

You might also like