Forward-declaring STL container types Some time ago, when look­ing in­to dif­fer­ent ways how to re­duce STL head­er bloat, I dis­cov­ered that libc++’s <iosfwd> has a for­ward dec­la­ra­tion for std::vec­tor. Turns out, there are more of them — and not just on one par­tic­u­lar stan­dard li­brary im­ple­men­ta­tion.

Us­ing for­ward dec­la­ra­tions to avoid the need to #include the whole type def­i­ni­tion is one of the es­sen­tial tech­niques to counter the ev­er-in­creas­ing com­pile times. While it’s very com­mon to do for some li­braries (such as Qt), oth­er li­braries make it hard­er and some straight out im­pos­si­ble. If you want to learn more about for­ward dec­la­ra­tions, check out this archived post from 2013.

The C++ stan­dard ex­plic­it­ly for­bids for­ward-declar­ing types from the stan­dard li­brary on the us­er side, turn­ing it in­to an un­de­fined be­hav­ior. More­over, the STL names­pace usu­al­ly wraps an­oth­er inline names­pace, which is de­fined to some plat­form-spe­cif­ic string like std::__1 on libc++ or std::__ndk1 on An­droid, which makes it hard­er (but not im­pos­si­ble) to cor­rect­ly match the type dec­la­ra­tion on the oth­er side.

The nu­cle­ar so­lu­tion to this im­poss­bil­i­ty-to-for­ward-de­clare is to stop us­ing STL al­to­geth­er. Many per­for­mance-ori­ent­ed projects end­ed up go­ing that way, but, even though Mag­num is grad­u­al­ly pro­vid­ing al­ter­na­tives to more and more STL types, I don’t want to com­plete­ly alien­ate the users but rather give them a choice and a chance to use Mag­num with STL seam­less­ly.

A po­ten­tial so­lu­tion that isn’t an un­de­fined be­hav­ior could be ex­tend­ing the stan­dard li­brary im­ple­men­ta­tion it­self with for­ward dec­la­ra­tions for ad­di­tion­al types. For­ward dec­la­ra­tions for stream types are in the clas­sic <ios­fwd> head­er, but that’s about it for what the stan­dard guar­an­tees. And adding for­ward dec­la­ra­tions for oth­er types isn’t as straight­for­ward as it may seem.

The prob­lem with tem­plates Most con­tain­er types in the STL take op­tion­al tem­plate pa­ram­e­ters for an al­lo­ca­tor and oth­er things. For ex­am­ple, the full set of things need­ed for defin­ing the std::string type looks like this: // <string> namespace std { template < class > class allocator ; template < class > class char_traits ; template < class CharT , class Traits = char_traits < CharT > , class Allocator = allocator < CharT > > class basic_string { … }; typedef basic_string < char > string ; … } As with func­tions, the stan­dard man­dates that the de­fault val­ue is spec­i­fied on­ly once — ei­ther on a (for­ward) dec­la­ra­tion or on the def­i­ni­tion. So a way to for­ward-de­clare these is putting the de­fault tem­plate pa­ram­e­ter on a for­ward dec­la­ra­tion and then have the def­i­ni­tion with­out. This is by the way the main rea­son Mag­num pro­vides for­ward dec­la­ra­tion head­ers such as Cor­rade/Con­tain­ers/Con­tain­ers.h in­stead of sug­gest­ing peo­ple to write the for­ward dec­la­ra­tions on their own (it’s al­so much eas­i­er and less er­ror-prone when the type is a long typedef chain). For the std::string case above, it would mean that the def­i­ni­tion has to be split in­to two parts — for ex­am­ple, with <iosfwd> con­tain­ing the for­ward dec­la­ra­tion and all re­lat­ed typedef s, and <string> just the def­i­ni­tion. For the ac­tu­al def­i­ni­tion we have to in­clude the for­ward dec­la­ra­tion as well in or­der to get val­ues of the de­fault pa­ram­e­ters. // <iosfwd> namespace std { template < class > class allocator ; template < class > class char_traits ; template < class CharT , class = char_traits < CharT > , class = allocator < CharT > > class basic_string ; typedef basic_string < char > string ; … } // <string> #include <iosfwd> namespace std { template < class CharT , class Traits , class Allocator > class basic_string { … }; … } So, ba­si­cal­ly, if we’re able to find a for­ward dec­la­ra­tion of a STL con­tain­er type in­clud­ing the de­fault ar­gu­ments in some (even in­ter­nal) STL head­er, the head­er is her­met­ic and sig­nif­i­cant­ly small­er than the cor­re­spond­ing stan­dard head­er, we won. Con­verse­ly, if the type def­i­ni­tion con­tains the tem­plate de­fault pa­ram­e­ters, then we can be sure that no for­ward dec­la­ra­tion is pos­si­ble.

De­tect­ing what STL we’re on Be­cause we now wan­dered in­to the im­ple­men­ta­tion-spe­cif­ic ter­ri­to­ry, we need a way to de­tect what STL fla­vor is the code be­ing com­piled on. Then, for known STL im­ple­men­ta­tions with known for­ward dec­la­ra­tion head­ers we in­clude the par­tic­u­lar head­er, and use the full type def­i­ni­tion in the stan­dard head­er oth­er­wise. That means our for­ward dec­la­ra­tion wrap­per will al­ways work, but giv­ing us com­pi­la­tion time ad­van­tages in some cas­es. The clas­sic way to de­tect a STL ven­dor is to in­clude the <ciso646> head­er (which is de­fined to be emp­ty on C++) and then check for ei­ther _LIBCPP_VERSION (de­fined on libc++, used by Clang main­ly on mac­OS and iOS), _CPPLIB_VER (de­fined on MSVC STL, for­mer­ly Dinkumware) or __GLIBCXX__ (de­fined on GCC’s lib­st­dc++). One thing to note is that on GCC be­fore ver­sion 6.1 the <ciso646> head­er doesn’t de­fine the _LIBCPP_VERSION macro, so it’s need­ed to get it via some oth­er means. Be­gin­ning with C++20, there will be a new head­er, <ver­sion>, stan­dard­iz­ing this process. If you use Cor­rade and in­clude any of its head­ers, you’ll get the de­tect­ed li­brary ex­posed through one of the COR­RADE_­TAR­GET_LIBCXX, COR­RADE_­TAR­GET_LIB­ST­D­CXX or COR­RADE_­TAR­GET_DINKUMWARE macros.

What you can ex­pect The fol­low­ing ta­ble sum­ma­rizes what com­mon STL con­tain­er types can be for­ward-de­clared on which im­ple­men­ta­tion. The left col­umn shows pre­pro­cessed line count with GNU lib­st­dc++ and C++11 (un­less said oth­er­wise), gath­ered us­ing the fol­low­ing com­mand line: echo "#include <utility>" | gcc -std = c++11 -P -E -x c++ - | wc -l The oth­er col­umns then show how many pre­pro­cessed lines is the cor­re­spond­ing for­ward dec­la­ra­tion on a par­tic­u­lar im­ple­men­ta­tion, if ap­pli­ca­ble. libstdc++ 8.2 libc++ 7.0.1 MSVC STL 2017 std::pair

<utility> …

2 197 LoC …

4 265 LoC …

std::initializer_list

<initializer_list> …

53 LoC …

101 LoC …

std::list

<list> , 6 749 LoC ✘

✘

✘

std::forward_list

<forward_list> , 7 036 LoC ✘

✘

✘

std::string

<string> , 11 576 LoC ✔

<bits/stringfwd.h> , 47 LoC ✔

<iosfwd> , 741 LoC ✘

std::string_view

<string_view> , 7 876 LoC ✘

✘

✘

std::array

<array> , 15 003 LoC ✘

✔

<__tuple> , 2 455 LoC ✔

<utility> std::tuple

<tuple> , 13 444 LoC ✔

<type_traits> , 1 615 LoC ✔

<__tuple> , 2 455 LoC ✔

<utility> std::vector

<vector> , 8 613 LoC ✘

✔

<iosfwd> , 741 LoC ✘

std::span

<span> , 21 696 LoC ∅

✘

∅

std::[multi]set

<set> , 8 170 LoC ✘ ✘ ✘ std::[multi]map

<map> , 15 796 LoC ✘ ✘ ✘ std::unordered_[multi]set

<unordered_set> , 17 114 LoC ✘ ✘ ✘ std::unordered_[multi]map

<unordered_map> , 17 180 LoC ✘ ✘ ✘

Can we con­vince ven­dors to do this more? While I think it’s pos­si­ble to add ad­di­tion­al for­ward dec­la­ra­tions to some STL im­ple­men­ta­tions, it might not be al­ways pos­si­ble to do so with­out break­ing ABI com­pat­i­bil­i­ty — even in Mag­num I had to break ABI com­pat­i­bil­i­ty a few times in the past in or­der to achieve that (and now I know what to avoid to make new types eas­i­ly for­ward-de­clar­able from the start). The ide­al way would be to have the for­ward dec­la­ra­tions guar­an­teed by the stan­dard (ex­tend­ing <iosfwd> fur­ther, for ex­am­ple) so we don’t need to in­clude plat­form-spe­cif­ic in­ter­nal “bits”, but again this may cause an ABI break for many ven­dors and thus take years to im­ple­ment (like it hap­pened with std::string in C++11 where lib­st­dc++ could no longer have it copy-on-write — and the prob­lems it caused per­sist un­til to­day). There’s al­so a slight chance that, due to com­plex­i­ties of std::al­lo­ca­tor and oth­er types used in de­fault tem­plate ar­gu­ments, adding for­ward dec­la­ra­tions to <iosfwd> would make it no longer light­weight. This re­al­ly de­pends on how the im­ple­men­ta­tions are done and what all needs to be known to for­ward-de­clare giv­en type. Ques­tions? Com­plaints? Share your opin­ion on so­cial net­works: Twit­ter, Red­dit r/cpp, Hack­er News