January 08, 2023

Forwarding Headers in BSL

Introduction

This document provides a specification for rendering a forwarding header – that is, a header containing only forward declarations for types in an associated component. Forward declarations are often used to reduce compile time in instances where a type is used in name only (avoiding the need to #include the type’s header). However, forward declaring a type from a different unit-of-release is not permitted (see Coding Standards Rule 4.4.3 ).

This document outlines how library authors may create forward-declaration headers for their own components, allowing their users to take advantage of benefits to compile time of forward declarations, without incurring the restrictions that preclude forward declaring types between libraries. This document describes the planned content of a forwarding header, and the reasoning behind that content.

In addition, this document will give some guidance on which components might benefit from forwarding headers, and the set of initial components for which BDE plans to create forward declaration headers. Decisions about which types to provide forward declaration headers for do, however, rely on the engineering judgement of component owners and their users.

Goals and Guidelines

In order to better insulate both library providers and clients from library changes, without requiring clients to always include full headers even when complete class definitions are not necessary, BDE wants to provide forwarding headers as an optional part of our component definition which contain forward declarations of all of the forwardable names in the corresponding component header.

A few design requirements guided this process:

  • This proposal is an addition to the existing BDE coding conventions and component model.

    Changes to downstream tooling and general assumptions about file meanings and contents should be minimized. In order to be properly tested a forwarding header must be cyclically dependent with its primary header, so by necessity it is best served to be part of the same component.

  • The forwarding header needs to not force changes to existing or proposed tools and checks.

    This means a forwarding header name cannot have a form that matches that of a subordinate component.

  • The contents of the forwarding header should be entirely derivable from the contents of the primary header.

    This will facilitate user understanding as well as any future tooling for the generation and validation of forwarding headers.

  • Forwarding headers should not include any primary headers.

    Should a forwarding header need to reference names not in the same component it should only do so by including the forwarding header for the component that owns those names.

  • Only entity types that are relevant to BDE-style software should be considered for inclusion in BDE-style forwarding headers.

    This largely precludes any interest in forward declarations for namespace-scope enums or free functions. Future revisions of this document may make comments on features not used within BDE itself.

Guidance on Prioritizing Components for Forwarding Headers

While theoretically forwarding headers could be created for any component, they are particularly relevant to vocabulary types (i.e., types that are regularly passed by pointer or reference in function interfaces) with large compile times (either because of the complexity of the header itself or its dependencies).

BDE intends to start with an initial release of forwarding headers for types that we believe are extensively forward-declared across the Bloomberg codebase. This will likely consist of bdldfp_decimal, bslma_allocator and much of the bdlt package. Additional headers may also be added depending on feedback received.

Entity Types That Should Be Forwarded

  • Non-component-private namespace-scope class and struct names

    Component-private names should not be used outside a component, so there is no valid motivation to forward-declare them.

    Nested names are not definable in incomplete types, so non-namespace-scope class names are not relevant.

    It would be possible to limit this to non-utility class names, but there is extensive use of typedef for utility classes (such as frequently providing a short name within a class declaration for bslmf::MovableRefUtil such as MoveUtil) that do not require the named class be complete (until the typedef is actually used).

    Consistently knowing that if a name is not component-private (containing an _ for standard BDE-style components) and there is a forwarding header then that name will be in the forwarding header greatly reduces the need to actually open and read the forwarding header and the cognitive load of using forwarding headers.

  • Class templates with no default arguments

    Class template declarations are a commonly useful thing to have forward declared, as that can potentially reduce the need to include large headers with the full class definitions.

    Default template arguments, however, can only be declared on one declaration for a template, and they lead to both potential ODR violations and little utility if they are not themselves in the forwarding header. A common example is the default ALLOCATOR argument on many standard containers, which make use of the container quite cumbersome if it is not there. (No one wants to have to type out bsl::vector<int,bsl::allocator>.) It is possible, though, that a future iteration of this document will provide a format to write the defaults in the primary header but have the preproccessor only materialize those lines in the forwarding header.

    Simpler templates with no default arguments have no dependencies and are easy to add additional declarations for either before or after the primary declaration.

  • Type aliases (with typedef or using ) for entities that are forwardable

    Both typedef and using based type aliases can be redeclared any number of times, so duplicating the declarations in a forwarding header is trivial. Aliases such as these are used extensively within BDE to provide historical names for types that have been moved to a different component or had their names changed.

    Forward declarations of these aliases will significantly aid in performing renames in this fashion as it avoids needing to manually update all places where a name might be forward declared. Considering the amount of effort that went into previous migrations this is a strong motivator.

Entity Types That Should Not Be Forwarded

  • Component private class names

    Since these should not be directly used outside of the component that declares them there is no merit in providing forward declarations for them.

  • Class templates with default parameters

    Default parameters for a class template can only be specified once in a translation unit. This means that the forwarding header would have to either declare the template with no default parameters or declare the default parameters and have them removed from the primary header (since the two headers intrinsically need to be able to coexist). Not providing defaults is aggressively user-hostile (especially for cases where the default is used for SFINAE with defaulted parameters, or for parameters like container allocator types or comparison functions, where non-default values are generally only chosen by expert users). Providing defaults requires making the forwarding header in some way authoritative, and could conceivably be done with bespoke conditional compilation between the primary header and the forwarding header, but at this stage we are forgoing developing such a solution.

  • Template specializations

    These are not needed either, as any use of the primary template will need to include the primary header, and thus would pull in the associated specializations.

  • Free functions and operators

    For now we are choosing not to forward any free functions. They are generally used only minimally with the BDE codebase, and those that do exist are predominantly useless when their associated types are not complete.

  • Global scope enums

    These do not exist within the BDE coding style. Were we to have any of them we would likely not forward declare them as they are either not redeclarable. C++11 enum class types could be forward declared, but for this stage we are postponing any action on that as they are unused within the BDE codebase (and functionally unusable while we still support C++03).

  • Global constants

    These are similarly not used within the BDE coding style. Should we have a need to consider them we are open to doing so.

  • Macros of any sort

    Macros not being redeclarable makes forwarding them infeasible.

Changes to Existing Headers

Two changes to existing files in a component should be made:

  • Include the forwarding header in the primary header.

    The first non-comment line within the include guards of the primary (.h) header of the component shall be an inclusion of the forwarding header.

    While it is not strictly necessary to do this, as the various implementation files associated with a component can validate that the forwarding header is reasonably well formed, this is still the safest approach to being sure that there is not a drift between available names and observed behavior when a forwarding header is included or not included - the case where a component’s forwarding header is not included when using a component is simply not possible. Changing this decision would require thorough testing of component behavior both when a forwarding header has been included and when it has not.

    This inclusion causes the primary .cpp file to validate that the forwarding header compiles on its own when included as the first non-vacuous statement in a file (and thus has no dependencies).

    abc_component.h
    Before
    // ... Documentation
    
    
    
    // ... Rest of includes
    
    After
    // ... Documentation
    
    #include <abc_component.fwd.h>
    
    // ... Rest of includes
    
  • Also include the forwarding header in the test driver.

    The second non-vacuous statement of the test driver should be an include of the forwarding header (the first non-vacuous statement being an include of the primary header).

    This verifies that the reverse include order - primary header then forwarding header - compiles with no issues and does not break a component.

    abc_component.t.cpp (first line truncated to fit on page)
    Before
    1// abc_component.t.cpp                    ... -*-C++-*-
    2#include <abc_component.h>
    3
    4
    5// ... Rest of includes
    
    After
    1// abc_component.t.cpp                    ... -*-C++-*-
    2#include <abc_component.h>
    3#include <abc_component.fwd.h>
    4
    5// ... Rest of includes
    

Forwarding Header Structure and Layout

Every component abc_component may optionally provide a forwarding header with the extension .fwd.h named abc_component.fwd.h.

The code for the following example can be found at bslma_allocator.fwd.h

bslma_allocator.fwd.h
// bslma_allocator.fwd.h                                              -*-C++-*-
#ifndef FWD_INCLUDED_BSLMA_ALLOCATOR
#define FWD_INCLUDED_BSLMA_ALLOCATOR

//@PURPOSE: Provide forward declarations for memory-allocation mechanisms.
//
//@CLASSES:
//  bslma::Allocator: protocol class for memory allocation and deallocation
//
//@SEE_ALSO: bslma_allocator
//
//@DESCRIPTION: This header provides declarations for those top-level names in
// this component that are intended to be usable in name only.  See below for
// the declared names and see 'bslma_allocator.h' for documentation on those
// names.

namespace BloombergLP {
namespace bslma {

class Allocator;

}  // close package namespace
}  // close enterprise namespace

#endif

// ----------------------------------------------------------------------------
// Copyright 2023 Bloomberg Finance L.P.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ----------------------------- END-OF-FILE ----------------------------------

Note that a BDE forwarding header will meet MOST of the requirements of a primary component header:

  • Include guards

    The include guards for this header need to not conflict with both the primary component header and with a theoretical _fwd subordinate component. To facilitate this we instead prefix the include guard name for the component with FWD_

  • Documentation

    Each forwarding header will then contain a boilerplate piece of documentation describing the purpose of the header, mostly indicating the purpose of the header and pointing to the primary header for all actual documentation.

  • Declarations

    The actual forward declarations will then be placed in the appropriate namespace.

Testing Goals

The following salient aspects of a forwarding header need to be tested to verify that the header will work correctly, and many (but not all) of these are accomplished without any need to build and run a distinct test driver file

  • The forwarding header must compile properly when it is the first part of a translation unit (as must be true for all headers).

    This is checked by including the forwarding header as the first non-vacuous statement in the primary header, so the primary .cpp file will include the forwarding header as its first non-vacuous content.

  • The forwarding header must work properly when included BEFORE the primary header within a translation unit.

    This check is also accomplished by always including the forwarding header at the start of the primary header when it exists, along with hopefully validating that the forwarding header include guards work correctly (which is done by bde_verify).

  • The forwarding header must work properly when included AFTER the primary header.

    This is again primarily checked by the primary header including the forwarding header combined with having working include guards in the forwarding header. This is doubly checked by the primary test driver including the forwarding header after the primary header, thus increasing the chance of catching invalid include guards on the forwarding header.

  • The forwarding header should declare all names from the primary header that should be forwarded.

    A test driver for the forwarding header, abc_component.fwd.t.cpp, will be deployed to validate this.

  • All names forwarded by the forwarding header should be complete when the primary header is included.

    A test driver for the forwarding header, abc_component.fwd.t.cpp, will be deployed to validate this.

Forwarding Header Test Driver Structure and Layout

The forwarding header test driver will be a somewhat standard BDE test driver with the following changes ( The code for this example can be found at bslma_allocator.fwd.t.cpp )

Only the forwarding header and basic test support headers (bslim_testutil.h, stdio, and cstdlib) will be included before the test driver code.

bslma_allocator.fwd.t.cpp include files
// bslma_allocator.fwd.t.cpp                                          -*-C++-*-
#include <bslma_allocator.fwd.h>

#include <bslim_testutil.h>

#include <bsl_iostream.h>
#include <bsl_ostream.h>

#include <bsl_cstdlib.h>
#include <bsl_cstring.h>

using namespace BloombergLP;
using bsl::cout;
using bsl::cerr;
using bsl::endl;
using bsl::atoi;

A local support template, isIncomplete, will be defined in an anonymous namespace:

bslma_allocator.fwd.t.cpp isIncomplete
// ============================================================================
//               STANDARD FORWARDING HEADER TESTING UTILITIES
// ----------------------------------------------------------------------------

namespace {

template <class t_TYPE> bool isIncomplete(int(*)[sizeof(t_TYPE)]);
template <class t_TYPE> bool isIncomplete(...);
    // Return 'true' when invoked with literal '0' argument if 't_TYPE' is
    // incomplete, 'false' if 't_TYPE' is complete.

template <class t_TYPE>
bool isIncomplete(int(*)[sizeof(t_TYPE)])
{
    return false;
}

template <class t_TYPE>
bool isIncomplete(...)
{
    return true;
}

}  // close unnamed namespace

A special function testTypeCompleteness will be forward-declared before main

bslma_allocator.fwd.t.cpp testTypeCompleteness
// ============================================================================
//                         NAME COMPLETION DECLARATION
// ----------------------------------------------------------------------------

static void testTypeCompleteness();
    // Verify that the names declared in this forwarding header are complete
    // when the primary header is included.  Note that the definition of this
    // function is at the end of this file after 'main' and the inclusion of the
    // primary header.

A standard BDE test drive main will be defined, with one test case that verifies that all names that should be forwarded are defined and incomplete

bslma_allocator.fwd.t.cpp single test case
    case 1: {
  // --------------------------------------------------------------------
  // FORWARD DECLARATIONS
  //
  // Concerns:
  //: 1 Each type that should be available has been declared by having
  //:   included the forwarding header for this component.
  //:
  //: 2 The types are incomplete.
  //:
  //: 3 The types are completed by including the primary header.
  //
  // Plan:
  //: 1 Verify that each expected name is declared and is incomplete
  //:   using 'isIncomplete'.
  //:
  //: 2 Invoke 'testTypeCompleteness', defined below after the inclusion of
  //:   the primary header, to verify that the forward names are completed
  //:   by including the primary header.
  // --------------------------------------------------------------------

  if (verbose) printf("\nFORWARD DECLARATIONS"
                      "\n====================\n");

  ASSERT(isIncomplete<BloombergLP::bslma::Allocator>(0));

  testTypeCompleteness();

Finally, after main (and most importantly, after the test case where the names being forwarded are verified to be incomplete) the primary header will be included and the testTypeCompleteness function will be defined to verify that the names needed are made complete by the primary header:

bslma_allocator.fwd.t.cpp definition of testTypeCompleteness
// Verify that the primary header can be included *after* the forwarding
// header.
#include <bslma_allocator.h>

static void testTypeCompleteness()
{
    ASSERT(!isIncomplete<BloombergLP::bslma::Allocator>(0));
}

While we cannot verify that ALL appropriate names are forward declared, this approach does verify that those which we know should be forwarded are forwarded correctly by the forwarding header and then defined by the primary header.

These checks could also be done as a static_assert when available, or using BSLMF_ASSERT, but we want to minimize the code dependencies of this pattern in order to make it viable at every level in the BDE codebase.

Client Usage

For clients of a library providing bde-style forwarding headers there are a few important things to consider.

  • Once a forwarding header is available, will it always be available and contain at least the same set of declarations?

    As with all changes to BDE, the intention when adding any forwarding header is that it will always be available and declarations added thereto will not be subsequently removed without good reason. We suspect this will be true for any responsible library provider.

  • When should I use a forwarding header?

    Below are two primary use cases for when to use forwarding headers that every developer should be aware of and consider as an option.

Replacing Forward Declarations

The most frequent expected use for forwarding headers, is to replace already existing cross-package forward declarations (which are not permitted per the Coding Standards Rule 4.4.3 ) with an include of the corresponding forwarding header instead. For example, consider the following change:

my_header.h
Before
// my_header.h

#ifndef INCLUDED_MY_HEADER
#define INCLUDED_MY_HEADER

// ... Documentation

// ... Some includes


namespace BloombergLP {

namespace bslma { class Allocator; }

// ... The rest of my header

}  // close enterprise namespace

#endif
After
// my_header.h

#ifndef INCLUDED_MY_HEADER
#define INCLUDED_MY_HEADER

// ... Documentation

// ... Some includes
#include <bslma_allocator.fwd.h>

namespace BloombergLP {



// ... The rest of my header

}  // close enterprise namespace

#endif

This would enable the providers of bslma::Allocator to freely update that declaration to something which might not be compatible, such as changing it to a struct or a typedef, without any impact on the owners of my_header.

Replacing Full Includes with Forwarding Headers

The other potential beneficiary of a forwarding header is the library client who wants to minimize the overhead of their header on their own clients by only including the needed level of dependency on a component that provides a forwarding header.

Consider again a library which only uses the name of bslma::Allocator in its header and does not depend on its definition. This component imposes on all clients the full include of bslma_allocator.h, and more so imposes a dependency on the physical structure of that header and not only on the names it provides.

This same code can instead be modified to only utilize the forwarding header in its own header:

my_util.h
Before
// my_util.h

#ifndef INCLUDED_MY_UTIL
#define INCLUDED_MY_UTIL

// ... Documentation

// ... Some includes
#include <bslma_allocator.h>

namespace BloombergLP {

namespace my {

struct Util {

static int doStuff(bslma::Allocator *allocator);
   // Do stuff using the specified 'allocator'
   // to provide memory.

};

}  // close package namespace
}  // close enterprise namespace

#endif
After
// my_util.h

#ifndef INCLUDED_MY_HEADER
#define INCLUDED_MY_UTIL

// ... Documentation

// ... Some includes
#include <bslma_allocator.fwd.h>

namespace BloombergLP {

namespace my {

struct Util {

static int doStuff(bslma::Allocator *allocator);
   // Do stuff using the specified 'allocator'
   // to provide memory.

};

}  // close package namespace
}  // close enterprise namespace

#endif

In addition, the implementation file can be modified add the inclusion of the component’s primary header:

my_util.cpp
Before
// my_util.cpp

#include <my_util.h>



namespace BloombergLP {

int my::Util::doStuff(bslma::Allocator *allocator)
{
    // ... Make actual use of 'allocator'.
    return 0;
}

}  // close enterprise namespace
After
// my_util.h

#include <my_util.h>

#include <bslma_allocator.h>

namespace BloombergLP {

int my::Util::doStuff(bslma::Allocator *allocator)
{
    // ... Make actual use of 'allocator'.
    return 0;
}

}  // close enterprise namespace

This reduces the coupling between the component depended upon (bslma_allocator) and clients of my_util, without any risk of additional maintenance burdens.

Future Considerations

Here are examples of further work we may wish to consider in the future:

  • Automate forwarding header generation.

    If the demand for forwarding headers is sufficient, we may wish to create tooling to generate, validate, and maintain these headers so that developers do not have to manually update them on each change.

  • Create forwarding headers for entire packages or package groups.

    A related feature that is often requested is to provide forwarding headers for entire packages or package groups. This is generally only useful with forwarding headers, as otherwise you create a single header that brings in far too many dependencies without any concrete benefit.

  • Automate resulting changes to client code.

    Another potential piece of followup work would be to look at tooling to assist with the creation of pull requests to replace existing external forward declarations by inclusions of the corresponding forwarding headers.