2

The C++ standard since C++11 guarantees that std::cout is "available for use in the constructors and destructors of static objects with ordered initialization (as long as is included before the object is defined)" (quoting from cppreference.com). I don't go into the details here (like, the role of std::ios_base::Init etc.). See for example Is it safe to use standard library functions before main() has been called?

This guarantee, however, implies that whenever <iostream> is included, the compiler has to ensure that some initialization code is added to the object file (unless there exist some optimizations by compiler/linker that make it possible to avoid this). I tried this with the Godbolt Compiler Explorer: For ARM gcc 5.4(linux) and -O2, the following code

int main() {
}

compiles to

main:
        mov     r0, #0
        bx      lr

whereas the code

#include <iostream>
int main() {
}

compiles to

main:
        mov     r0, #0
        bx      lr
_GLOBAL__sub_I_main:
        stmfd   sp!, {r4, lr}
        ldr     r4, .L4
        mov     r0, r4
        bl      std::ios_base::Init::Init() [complete object constructor]
        mov     r0, r4
        ldr     r2, .L4+4
        ldr     r1, .L4+8
        bl      __aeabi_atexit
        ldmfd   sp!, {r4, lr}
        bx      lr
.L4:
        .word   .LANCHOR0
        .word   __dso_handle
        .word   _ZNSt8ios_base4InitD1Ev
.LANCHOR0 = . + 0

Consequently, the mere inclusion of <iostream> increases the code size and initialization time. For an individual file the impact might be considered as negligible. However, adding such include directives needlessly also to widely used library header files would IMO still count as avoidable waste of resources. I see it as another (even if not strong) argument for keeping your include directives clean.

That said, my question is, are there other header files defined by the standard (preferably latest version) that will also by mere inclusion (that is, without any actual reference to the contents of the header file) cause some code / data to be added to the resulting object file? Note that I do not limit this question to initialization scenarios - there might be other reasons.

Some additional notes:

  • There could be impacts on symbol table size. This is not of interest for me - my interest is in code size, data size and performance.
  • I am aware that a non-optimizing compiler might produce code (out-of-line) for inline functions even if the inline function is never called. You can assume that optimizations are enabled that prevent this to happen.
Dirk Herrmann
  • 5,550
  • 1
  • 21
  • 47
  • 1
    I don't think the standard makes guarantees either way. You already seem to know how check whether a header causes additional code generation, so how about you tell us which headers do so? – eerorika May 03 '19 at 11:46
  • Looks like you are bumping up against the C++ Nifty Counter idiom, also known as a Schwarz Counter. https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Nifty_Counter – Eljay May 03 '19 at 12:08
  • 1
    @eerorika: Took your advice and just did the experiment you proposed. See below. – Dirk Herrmann May 03 '19 at 18:36

3 Answers3

1

This isn't really in scope of the standard, but it is true that a sensible implementation of IOStream mandates this initialisation code (otherwise std::cout wouldn't be available, though there is some other static state that's shared around).

I haven't personally encountered this with other parts of the library, and I can't think of a reason for containers or algorithms to do it. I can imagine some of the threading subsystem might involve some up-front initialisation.

Ultimately, the only way you can know, for your toolchain and platform, is to try it. A quick script that generates C++ source files including various standard headers in turn, passes them to your compiler in turn and inspects the resulting assembly, will reveal the answer in short-ish order.

Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055
1

As @eerorika rightfully commented, I could use the mechanism I have used for <iostream> with all other standard headers myself. So, I tried the following piece of code with each header from https://en.cppreference.com/w/cpp/header (except for the experimental headers and the C compatibility headers). Again, I used the Godbolt Compiler Explorer (https://godbolt.org/), this time with ARM gcc 8.2 and options -O2 -std=c++17:

#include<xxx>  <--- xxx was exchanged for each of the headers below
int main() {
}

Here's the list of headers: cstdlib, csignal, csetjmp, cstdarg, typeinfo, typeindex, type_traits, bitset, functional, utility, ctime, chrono, cstddef, initializer_list, tuple, any, optional, variant, new, memory, scoped_allocator, climits, cfloat, cstdint, cinttypes, limits, exception, stdexcept, cassert, system_error, cerrno, cctype, cwctype, cstring, cwchar, cuchar, string, string_view, charconv, array, vector, deque, list, forward_list, set, map, unordered_set, unordered_map, stack, queue, iterator, algorithm, cmath, complex, valarray, random, numeric, ratio, cfenv, iosfwd, ios, istream, ostream, iostream, fstream, sstream, strstream, iomanip, streambuf, ostream, cstdio, locale, clocale, codecvt, regex, atomic, thread, mutex, shared_mutex, future, condition_variable, filesystem

Some seemed not to be supported (some C++17, some C++20), so I could not try them: compare, version, memory_resource, contract, span, ranges, execution, bit, syncstream

Result: Only for <iostream> I got the abovementioned extra content in the assembly code.

Dirk Herrmann
  • 5,550
  • 1
  • 21
  • 47
0

Back in the olden days, Gerry Schwarz invented the "nifty counter" to ensure initialization of standard stream objects before any possible use. That did, indeed, add overhead to every translation unit that did #include <iostream>.

But that was in large part because of the limitations of cfront: it compiled C++ code to C, and then relied on the native compiler to translate that C code into an executable file.

These days we have native compilers for C++, and they can do things that cfront couldn't easily do. In particular, they typically have some sort of more-or-less sophisticated built-in mechanism for initializing things before entry into main. That typically uses a table of functions to be called from the startup code; each translation unit adds to that table, through the linker, for global and file-scope objects that have to be initialized before main.

It's a straightforward extension to that table mechanism to provide priorities among the various startup functions, and initializing the standard streams gets a higher priority than initializing user-defined objects, so the startup mechanism ensures that the streams are initialized before any code that uses them. That's done in the standard library implementation, so the code is only in one place; if the linker pulls in the stream libraries it also gets the stream initialization code. No need for any extra information in any translation unit.

Borland's compiler, for example, allowed 256 different priorities. Only a few of those priorities were used, but the mechanism allowed fine-tuned control of startup order.

In short: including a header file does not require generating extra initialization code; that's an implementation detail, and for startup there are better ways to do it.

Pete Becker
  • 74,985
  • 8
  • 76
  • 165