How g++ injects -lstdc++
Source references in this chapter are to GCC 15.2.0 (releases/gcc-15.2.0).
You have a project of C object files. Nothing in it is C++. You link it with g++
because that is what the build system happens to use, and you add -lz because you
need zlib. The link line that comes out has -lstdc++ on it, and — more
disconcertingly — your -lm has moved. You typed it before -lz; it comes out
after -lstdc++.
g++ is the same binary logic as gcc with one extra function called before any
option is acted on. That function may rewrite the entire option array: insert
options, delete them, and reorder them. Everything surprising about C++ linking
happens in those few hundred lines, and none of it is in the manual.
This chapter reads gcc/cp/g++spec.cc end to end. Chapter 1.8 showed what the
generic driver does with a link line; this is what has already happened to that line
before the driver sees it.
One hook, called before anything
The generic driver decodes argv into an array of struct cl_decoded_option, and
then — before acting on a single one of them — calls into the front end:
/* Do language-specific adjustment/addition of flags. */
lang_specific_driver (&decoded_options, &decoded_options_count,
&added_libraries);
Listing 11-1: the only hook this chapter is about
(gcc/gcc.cc:4860-4862).
Note the addresses-of. All three arguments are out-parameters: the callee may
replace the array wholesale, change its length, and report how many libraries it
added. Every front end links a lang_specific_driver; C's is a no-op, and C++'s is
gcc/cp/g++spec.cc. That is the entire difference between gcc and
g++.
The consequence is worth stating before the mechanism: by the time the specs run,
-lstdc++ is an ordinary -l, indistinguishable from one you typed. No spec
mentions it. Grepping -dumpspecs for stdc++ finds nothing.
One variable with four values
The whole decision collapses into a single int, and its own comment is the
clearest documentation that exists:
/* What action to take for the c++ runtime library:
-1 means we should not link it in.
0 means we should link it if it is needed.
1 means it is needed and should be linked in.
2 means it is needed but should be linked statically. */
int library = 0;
Listing 11-2: gcc/cp/g++spec.cc:92-97.
The default is 0 — link it if needed — and the whole first pass over the options
exists to move it off zero. Two things follow from the initial value being the
"maybe" state rather than "no": most transitions are 0 → 1, and they are guarded
by if (library == 0) so that an earlier -1 cannot be overwritten.
Alongside library there is a parallel array of per-argument bits, one int per
option:
/* This bit is set if we saw a `-xfoo' language specification. */
#define LANGSPEC (1<<1)
/* This bit is set if they did `-lm' or `-lmath'. */
#define MATHLIB (1<<2)
/* This bit is set if they did `-lc'. */
#define WITHLIBC (1<<3)
/* Skip this option. */
#define SKIPOPT (1<<4)
/* Add -lstdc++exp for experimental features that need library support. */
#define EXPERIMENTAL (1<<5)
Listing 11-3: the per-argument bits (gcc/cp/g++spec.cc:26-35).
Four of those five are set and tested. EXPERIMENTAL is never used — the
#define is its only occurrence in the file, at every release from 13.3.0 through
15.2.0. -fcontracts is tracked by a separate bool need_experimental instead. It
is the first of two pieces of vestigial state you will meet in this chapter; both are
worth knowing about only so that you stop looking for what sets them.
What turns injection on
Two paths reach library = 1, and both are broader than people expect.
Any -l you do not recognise
case OPT_l:
if (strcmp (arg, MATH_LIBRARY) == 0)
{
args[i] |= MATHLIB;
need_math = 0;
}
else if (strcmp (arg, "c") == 0)
args[i] |= WITHLIBC;
else
/* Unrecognized libraries (e.g. -lfoo) may require libstdc++. */
library = (library == 0) ? 1 : library;
break;
Listing 11-4: gcc/cp/g++spec.cc:178-189.
There are exactly two recognised libraries — MATH_LIBRARY ("m" by default) and
"c". Everything else is an unknown, and an unknown might need the C++ runtime. So
g++ -lz foo.o links libstdc++ because of -lz, on the strength of the source
comment quoted above and nothing more.
-Xlinker and -Wl, do the same thing, with a comment that spells out the
reasoning (:207-213): "Arguments that go directly to the linker might be
.o files, or something, and so might cause libstdc++ to be needed."
Any input file that is not a header
This is the rule that folklore gets wrong. It is not "a C++ source file on the command line". It is a negative test on the filename:
/* If we don't know that this is a header file, we might
need to be linking in the libraries. */
if (library == 0)
{
if ((len <= 2 || strcmp (arg + (len - 2), ".H") != 0)
&& (len <= 2 || strcmp (arg + (len - 2), ".h") != 0)
&& (len <= 4 || strcmp (arg + (len - 4), ".hpp") != 0)
...
&& (len <= 3 || strcmp (arg + (len - 3), ".hh") != 0))
library = 1;
}
Listing 11-5: abridged from gcc/cp/g++spec.cc:277-291.
Nine header suffixes are checked — .H, .h, .hpp, .hp, .hxx, .h++,
.HPP, .tcc, .hh — and anything else sets library = 1. There is no test
for a C++ extension anywhere in the function. g++ foo.o, g++ foo.a,
g++ foo.s all link the C++ runtime, because none of those is a header.
So the accurate rule is: g++ links libstdc++ unless you gave it nothing but
headers, or explicitly told it not to. The driver is deliberately conservative,
and the trade is sound — a missing -lstdc++ is a confusing undefined-symbol
error, a spurious one costs a DT_NEEDED entry.
You can watch the two paths independently:
$ g++ -### foo.o 2>&1 | tr ' ' '\n' | grep -c stdc # 1 — input file rule
$ g++ -### foo.hh 2>&1 | tr ' ' '\n' | grep -c stdc # 0 — header, no link anyway
$ gcc -### foo.o -lz 2>&1 | tr ' ' '\n' | grep -c stdc # 0 — gcc injects nothing
Listing 11-6: the injection rules, one command each.
What turns injection off
Chapter 1.10 covered the link-line half of -nostdlib. Here is its C++ half:
case OPT_nostdlib__:
args[i] |= SKIPOPT;
/* FALLTHRU */
case OPT_nostdlib:
case OPT_nodefaultlibs:
library = -1;
break;
Listing 11-7: gcc/cp/g++spec.cc:170-176.
And separately, the options under which no link happens at all:
case OPT_c:
case OPT_r:
case OPT_S:
case OPT_E:
case OPT_M:
case OPT_MM:
case OPT_fsyntax_only:
/* Don't specify libraries if we won't link, since that would
cause a warning. */
library = -1;
break;
Listing 11-8: gcc/cp/g++spec.cc:215-225.
Listing 11-8 is why g++ -c foo.cc produces no unused-argument noise, and why -r
lands at -1 even though it is absent from Listing 11-7.
Note what is not in either list: -nostartfiles. The truth table from Chapter
1.10 is asymmetric in its fourth column precisely because that column is decided
here, by two case labels, rather than by the %{!…:} guards in
LINK_COMMAND_SPEC.
-nostdlib++ is the one that vanishes. It takes SKIPOPT before falling through,
which means the option is deleted from the array at :368-369 and never
reaches the specs. So it removes the C++ runtime and leaves -lc and -lgcc
completely alone — which is exactly what its name promises and what -nostdlib
cannot do.
If you are on GCC 12 or earlier,
-nostdlib++does not exist. It arrived in GCC 13, along with-lstdc++exp. The same commit took this file from 436 to 460 lines, so every line number in this chapter differs before GCC 13 — and before GCC 12 the file isg++spec.c, not.cc.
The reordering nobody documents
Now the part that changed your command line. In the copy loop, two options are plucked out as they go past:
/* Make sure -lstdc++ is before the math library, since libstdc++
itself uses those math routines. */
if (!saw_math && (args[i] & MATHLIB) && library > 0)
{
--j;
saw_math = &decoded_options[i];
}
if (!saw_libc && (args[i] & WITHLIBC) && library > 0)
{
--j;
saw_libc = &decoded_options[i];
}
Listing 11-9: gcc/cp/g++spec.cc:326-338.
The --j is the removal. The option was already written to
new_decoded_options[j] at the top of the loop; decrementing j means the next
iteration overwrites it. The option is stashed in a pointer and re-emitted at the
end, after the runtime:
if (saw_math)
new_decoded_options[j++] = *saw_math;
else if (library > 0 && need_math)
{ ... generate_option (OPT_l, MATH_LIBRARY, ...) ... }
if (saw_time)
new_decoded_options[j++] = *saw_time;
if (saw_libc)
new_decoded_options[j++] = *saw_libc;
Listing 11-10: the re-emission (gcc/cp/g++spec.cc:430-443).
Three things to take from this.
The reordering is real and silent. g++ -lm foo.o links foo.o -lstdc++ -lm.
On a target where everything is a static archive, ld resolves left to right and
order determines which symbols get pulled in, so this is not cosmetic. The reason
is correct — libstdc++.a has undefined references into libm — but the
consequence is that you cannot put -lm before -lstdc++ through the g++
driver at all. If you need that, drive the link with gcc or ld and supply the
C++ runtime yourself.
A missing -lm is synthesised. The else if branch adds one when you did not.
It is gated on need_math, which is (MATH_LIBRARY[0] != '\0') at
:129 — so a port that folds the maths routines into libc defines
MATH_LIBRARY "" and no -lm appears. Check this before concluding that a new
target's link line is too short.
The -lrt slot is dead code. saw_time is initialised to NULL at
:118-119, read at Listing 11-10, and assigned nowhere in the
file — there is no TIMELIB bit to go with MATHLIB and WITHLIBC. Verified
identical at every release from 11.4.0 to 15.2.0. Exactly two of your options ever
get hoisted: -lm and -lc. This is the second piece of vestigial state promised
earlier.
$ g++ -### -lm -lz foo.o 2>&1 | tr ' ' '\n' | grep -E '^-l|stdc'
Listing 11-11: watch -lm come out after -lstdc++, and -lz stay where you put
it.
-static-libstdc++ changes no path and no filename
The option does not affect the library name, the search order, or any -L. It wraps
the one -l in a pair of linker state changes:
… -Wl,-Bstatic -lstdc++ -Wl,-Bdynamic -lm -lc
That is all it is: flip ld into archive-preference mode for exactly one library,
then flip it back. The two halves are emitted around the -l:
#ifdef HAVE_LD_STATIC_DYNAMIC
if (library > 1 && !static_link)
{
generate_option (OPT_Wl_, LD_STATIC_OPTION, 1, CL_DRIVER,
&new_decoded_options[j]);
j++;
}
#endif
Listing 11-12: the opening half (gcc/cp/g++spec.cc:384-391); the
closing LD_DYNAMIC_OPTION is the mirror image at :421-428.
library > 1 is the 2 state, and !static_link skips the wrap under plain
-static, where everything is archive-only already. The option that sets the state
also deletes itself:
case OPT_static_libstdc__:
library = library >= 0 ? 2 : library;
#ifdef HAVE_LD_STATIC_DYNAMIC
/* Remove -static-libstdc++ from the command only if target supports
LD_STATIC_DYNAMIC. When not supported, it is left in so that a
back-end target can use outfile substitution. */
args[i] |= SKIPOPT;
#endif
break;
Listing 11-13: gcc/cp/g++spec.cc:235-243.
Read the library >= 0 guard: g++ -c -static-libstdc++ stays at -1, because
-c got there first. And read the #ifdef: on a target without
HAVE_LD_STATIC_DYNAMIC the option is not deleted, so it survives into the
specs for the port to act on. That conditional is why two targets disagree about
whether the option appears in -v output.
Two consequences that bite in practice:
libstdc++.a has to exist in the same search path. Nothing about the search
changed — only ld's preference between .a and .so. If you ship a cross
toolchain with a shared-only libstdc++, this option silently gives you the shared
one and the link succeeds.
The option does not appear in the link line on most targets. Do not grep -v
output for -static-libstdc++; grep for Bstatic:
$ g++ -### -static-libstdc++ foo.o 2>&1 | tr ' ' '\n' | grep -E 'Bstatic|Bdynamic|stdc'
Listing 11-14: the option is gone; its effect is two -Wl, options.
The exact spelling is configure-probed, in a case "$target" at
gcc/configure.ac:4231-4250:
| Value | Target |
|---|---|
-Bstatic / -Bdynamic | GNU ld, Solaris — the default |
-bstatic / -bdynamic | AIX |
-aarchive_shared / -adefault | HP-UX (non-GNU ld) |
with the three AC_DEFINEs at :4253-4259.
Which library, exactly
The name is a macro, so a port can change it:
#ifndef LIBSTDCXX
#define LIBSTDCXX "stdc++"
#endif
#ifndef LIBSTDCXX_PROFILE
#define LIBSTDCXX_PROFILE LIBSTDCXX
#endif
#ifndef LIBSTDCXX_STATIC
#define LIBSTDCXX_STATIC NULL
#endif
Listing 11-15: gcc/cp/g++spec.cc:44-52.
LIBSTDCXX_PROFILE is chosen when saw_profile_flag is set by -p or -pg
(:407-410). LIBSTDCXX_STATIC is not a replacement but an extra
library appended after the main one when linking statically
(:413-419) — the hook an RTOS or bare-metal port uses to add its
own support archive. If a link line on an unusual target does not say -lstdc++,
one of these three macros is why.
-fcontracts adds -lstdc++exp ahead of the main library
(:378-383), and — read the line numbers against Listing 11-12 — that
happens before the -Wl,-Bstatic. So -static-libstdc++ does not cover
libstdc++exp.
-stdlib=libc++ switches to clang's library, emitting -lc++ and -lc++abi
(:392-406). The ABI library is skipped when the port sets
LIBCXXABI to NULL, whose comment explains the case: a platform may forward the
ABI library from libc++ or combine it some other way. The option only exists if GCC
was configured for it — ENABLE_STDLIB_OPTION, which Chapter 1.12 covers.
The bookkeeping that stops g++ linking nothing
added_libraries is incremented for each synthesised -l and handed back through
the third out-parameter of Listing 11-1. The generic driver uses it once:
if (n_infiles == added_libraries)
fatal_error (input_location, "no input files");
Listing 11-16: gcc/gcc.cc:8970-8971.
Without that count, plain g++ with no arguments would see the libraries it had
just added itself, conclude it had inputs, and attempt a link. This is the only
place the return value is used, and it is the reason lang_specific_driver has to
report what it did rather than just doing it.
Documentation coverage
The options are documented — -static-libstdc++, -nostdlib++ and -stdlib= are
all in the manual. The mechanism is not, anywhere:
- The injection itself has no texi node. No document states that
g++adds-lstdc++, let alone under what conditions. Thelibrarystate machine exists only as the source comment in Listing 11-2. - The
-lfoorule is undocumented, and it is the one that catches people: linking any third-party library throughg++guarantees the C++ runtime. - The input-file rule is undocumented and is a header-suffix blacklist, not a C++-source test. Listing 11-5 is the only statement of it.
- The
-lm/-lchoist is undocumented. Nothing warns thatg++silently reorders your command line. -static-libstdc++'s mechanism is undocumented. The option is described; that it becomes-Wl,-Bstatic…-Wl,-Bdynamicand vanishes from the command line is not, and neither is theHAVE_LD_STATIC_DYNAMICconditional that decides whether it vanishes.
This is the largest documentation gap in Part I, which is why the chapter quotes so
much source: the comments in g++spec.cc are the specification.
Things that surprise people
gcc never injects anything. Link C++ objects with gcc and you get undefined
std:: symbols. That is the whole practical difference between the two drivers at
link time — not a different compiler, not different code generation.
g++ -lz foo.o links the C++ runtime even though nothing in it is C++.
Your -lm and -lc positions are not preserved. Everything else is.
-nostartfiles does not suppress -lstdc++. Two mechanisms, two lists.
A successful link says nothing about whether the program starts. Finding
libstdc++.so.6 at run time is the dynamic loader's job, driven by DT_NEEDED,
DT_RUNPATH and ld.so.conf — an entirely separate system from anything in this
chapter. Which libstdc++ the -l resolved to at link time is
Chapter 1.12.
All source references in this chapter are to GCC 15.2.0
(releases/gcc-15.2.0). Line numbers in other releases will differ; the surrounding
code rarely does. Where behaviour itself changed across a major version, it is
flagged inline.