31

Is there any case, where missing a #include would break the software at runtime, while the build still goes through?

In other words, is it possible that

#include "some/code.h"
complexLogic();
cleverAlgorithms();

and

complexLogic();
cleverAlgorithms();

would both build successfully, but behave differently?

5
  • 1
    Probably with your includes you could bring in your code redefined structures that are different than the ones used by the implementation of functions. This can lead to binary incompatibility. Such situations can't be handled by compiler and by the linker. Commented Mar 20, 2020 at 9:34
  • 11
    It certainly is. It is quite easy to have macros defined in a header that completely change the meaning of code that comes after that header is #included.
    – Peter
    Commented Mar 20, 2020 at 9:43
  • 4
    I'm sure Code Golf has done at least one challenge based on this.
    – Mark
    Commented Mar 20, 2020 at 21:20
  • 6
    I would like to point out a specific real-world example: The VLD library for memory leak detection. When a program terminates with VLD active, it will print out all the detected memory leaks on some output channel. You integrate it into a program by linking to the VLD library and placing a single line of #include <vld.h> in a strategic position in your code. Removing or adding that VLD header does not "break" the program, but it affects the runtime behavior significantly. I have seen VLD slow down a program to the point that it became unusable.
    – Haliburton
    Commented Mar 20, 2020 at 22:20
  • 2
    youtu.be/xVT1y0xWgww?t=1645
    – Damon
    Commented Mar 22, 2020 at 19:48

6 Answers 6

40

Yes, it's perfectly possible. I'm sure there are lots of ways, but suppose the include file contained a global variable definition which called a constructor. In the first case the constructor would execute, and in the second it wouldn't.

Putting a global variable definition in a header file is poor style, but it's possible.

1
  • 1
    <iostream> in the standard library does precisely this; if any translation unit includes <iostream> then the std::ios_base::Init static object will be constructed at program start, initializing the character streams std::cout etc., otherwise it will not.
    – ecatmur
    Commented Mar 22, 2020 at 13:02
34

Yes, that's possible.

Everything concerning #includes happens at compile time. But compile time things can change behavior at runtime, of course:

some/code.h:

#define FOO
int foo(int a) { return 1; }

then

#include <iostream>
int foo(float a) { return 2; }

#include "some/code.h"  // Remove that line

int main() {
  std::cout << foo(1) << std::endl;
  #ifdef FOO
    std::cout << "FOO" std::endl;
  #endif
}

With the #include, overload resolution finds the more appropriate foo(int) and hence prints 1 instead of 2. Also, since FOO is defined, it additionally prints FOO.

That's just two (unrelated) examples that came to my mind immediately, and I'm sure there are plenty more.

16

Just to point out the trivial case, precompiler directives:

// main.cpp
#include <iostream>
#include "trouble.h" // comment this out to change behavior

bool doACheck(); // always returns true

int main()
{
    if (doACheck())
        std::cout << "Normal!" << std::endl;
    else
        std::cout << "BAD!" << std::endl;
}

And then

// trouble.h
#define doACheck(...) false

It's pathological, perhaps, but I've had a related case happen:

#include <algorithm>
#include <windows.h> // comment this out to change behavior

using namespace std;

double doThings()
{
    return max(f(), g());
}

Looks innocuous. Tries to call std::max. However, windows.h defines max to be

#define max(a, b)  (((a) > (b)) ? (a) : (b))

If this was std::max, this would be a normal function call that evaluates f() once and g() once. But with windows.h in there, it now evaluates f() or g() twice: once during the comparison and once to get the return value. If f() or g() was not idempotent, this can cause problems. For example, if one of them happens to be a counter which returns a different number every time....

3
  • +1 for calling out Window's max function, a real world example of include implementation evil and a bane to portability everywhere.
    – Scott M
    Commented Mar 21, 2020 at 23:21
  • 4
    OTOH, if you get rid of using namespace std; and use std::max(f(),g());, the compiler will catch the problem (with an obscure message, but at least pointing to the call site).
    – Ruslan
    Commented Mar 22, 2020 at 8:09
  • @Ruslan Oh, yes. If given the chance, that's the best plan. But sometimes one is working with legacy code... (nope... not bitter. not bitter at all!)
    – Cort Ammon
    Commented Mar 23, 2020 at 13:23
4

It's possible to be missing a template specialization.

// header1.h:

template<class T>
void algorithm(std::vector<T> &ts) {
    // clever algorithm (sorting, for example)
}

class thingy {
    // stuff
};

// header2.h

template<>
void algorithm(std::vector<thingy> &ts) {
    // different clever algorithm
}

// main.cpp

#include <vector>
#include "header1.h"
//#include "header2.h"

int main() {
    std::vector<thingy> thingies;
    algorithm(thingies);
}
4

Binary incompatibility, accessing a member or even worse, calling a function of the wrong class:

#pragma once

//include1.h:
#ifndef classw
#define classw

class class_w
{
    public: int a, b;
};

#endif

A function uses it, and it is ok:

//functions.cpp
#include <include1.h>
void smartFunction(class_w& x){x.b = 2;}

Bringing in another version of the class:

#pragma once

//include2.h:
#ifndef classw
#define classw

class class_w
{
public: int a;
};

#endif

Using functions in main, the second definition changes the class definition. It leads to binary incompatibility and simply crashes at runtime. And fix the issue by removing the first include in main.cpp:

//main.cpp

#include <include2.h> //<-- Remove this to fix the crash
#include <include1.h>

void smartFunction(class_w& x);
int main()
{
    class_w w;
    smartFunction(w);
    return 0;
}

None of variants generates a compile or link time error.

The vice versa situation, adding an include fixes the crash:

//main.cpp
//#include <include1.h>  //<-- Add this include to fix the crash
#include <include2.h>
...

These situations are even much more difficult when fixing bug in an old version of program, or using an external library/dll/shared object. That's why sometimes must be followed the rules of binary backward compatibility.

7
  • The second header won't be included due to ifndef. Otherwise it won't compile (class redefining is not allowed).
    – Igor R.
    Commented Mar 20, 2020 at 10:28
  • @IgorR. Be attentive. The second header (include1.h) is the only included in first source code. This leads to binary incompatibility. That is exactly the purpose of the code, to ilustrate how an include can lead to crash at runtime. Commented Mar 20, 2020 at 10:37
  • 1
    @IgorR. this is very simplistic code, which illustrates such a situation. But in real life situation can be much more complicated subtile. Try patching some program without reinstalling the whole package. It is the typical situation where must be followed strictly the backward binary compatibility rules. Otherwise patching is an impossible task. Commented Mar 20, 2020 at 10:43
  • I'm not sure what "the first source code" is, but if you mean that 2 translation units have 2 different definitions of a class, it is ODR violation, i.e. undefined behavior.
    – Igor R.
    Commented Mar 20, 2020 at 14:27
  • 1
    That is undefined behavior, as described by the C++ Standard. FWIW, of course, it is possible to cause a UB this way...
    – Igor R.
    Commented Mar 20, 2020 at 15:06
3

I want to point out that the problem also exists in C.

You can tell the compiler a function uses some calling convention. If you don't, the compiler will have to guess that it uses the default one, unlike in C++ where the compiler can refuse to compile it.

For example,

main.c

int main(void) {
  foo(1.0f);
  return 1;
}

foo.c

#include <stdio.h>

void foo(float x) {
  printf("%g\n", x);
}

On Linux on x86-64, my output is

0

If you omit the prototype here, the compiler assumes you have

int foo(); // Has different meaning in C++

And the convention for unspecified argument lists requires that float should be converted to double to be passed. So although I gave 1.0f, the compiler converts it to 1.0d to pass it to foo. And according to System V Application Binary Interface AMD64 Architecture Processor Supplement, the double gets passed in the 64 least significant bits of xmm0. But foo expects a float, and it reads it from the 32 least significant bits of xmm0, and gets 0.

Not the answer you're looking for? Browse other questions tagged or ask your own question.