Может ли отсутствующий #include нарушить работу программы во время выполнения?

31

Есть ли какой-нибудь случай, когда пропадание #includeможет сломать программное обеспечение во время выполнения, в то время как сборка все еще проходит?

Другими словами, возможно ли, что

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

а также

complexLogic();
cleverAlgorithms();

будут ли оба успешно строить, но вести себя по-разному?

Antti_M
источник
1
Возможно, с вашими включениями вы могли бы внести в свой код переопределенные структуры, которые отличаются от тех, которые используются при реализации функций. Это может привести к двоичной несовместимости. Такие ситуации не могут быть обработаны компилятором и компоновщиком.
armagedescu
11
Это несомненно. Довольно просто определить макросы в заголовке, которые полностью меняют смысл кода, который следует после того, как заголовок #included.
Питер
4
Я уверен, что Code Golf сделал по крайней мере один вызов, основанный на этом.
Mark
6
Я хотел бы указать на конкретный пример из реальной жизни: библиотека VLD для обнаружения утечек памяти. Когда программа завершает свою работу с активным VLD, она распечатывает все обнаруженные утечки памяти на некотором выходном канале. Вы интегрируете его в программу, связываясь с библиотекой VLD и помещая одну строчку #include <vld.h>в стратегическую позицию в своем коде. Удаление или добавление этого заголовка VLD не «ломает» программу, но существенно влияет на поведение среды выполнения. Я видел, как VLD замедляет программу до такой степени, что она становится непригодной для использования.
Haliburton

Ответы:

40

Да, это вполне возможно. Я уверен, что есть много способов, но предположим, что включаемый файл содержит определение глобальной переменной, которая называется конструктор. В первом случае конструктор будет выполняться, а во втором - нет.

Помещение определения глобальной переменной в заголовочный файл - плохой стиль, но это возможно.

Джон
источник
1
<iostream>в стандартной библиотеке делает именно это; если какой-либо блок перевода включает в себя, <iostream>то std::ios_base::Initстатический объект будет создан при запуске программы, инициализируя потоки символов std::coutи т. д., иначе это не так.
Ecatmur
33

Да, это возможно

Все, что касается #includes, происходит во время компиляции. Но время компиляции может изменить поведение во время выполнения, конечно:

some/code.h:

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

тогда

#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
}

С #includeразрешением перегрузки находит более подходящее foo(int)и, следовательно, печатает 1вместо 2. Кроме того, поскольку FOOон определен, он дополнительно печатает FOO.

Это всего лишь два (не связанных) примера, которые сразу же пришли мне в голову, и я уверен, что их гораздо больше.

pasbi
источник
14

Просто чтобы указать на тривиальный случай, директивы прекомпилятора:

// 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;
}

А потом

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

Возможно, это патология, но у меня был похожий случай:

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

using namespace std;

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

Выглядит безобидно. Пытается позвонить std::max. Тем не менее, windows.h определяет max быть

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

Если бы это было так std::max, это был бы обычный вызов функции, который оценивает f () один раз и g () один раз. Но с windows.h теперь он оценивает f () или g () дважды: один раз во время сравнения и один раз, чтобы получить возвращаемое значение. Если f () или g () не были идемпотентами, это может вызвать проблемы. Например, если один из них является счетчиком, который каждый раз возвращает другое число ....

Корт Аммон
источник
+1 за вызов максимальной функции Window, реальный пример включения зла реализации и проклятия переносимости везде.
Скотт М
3
OTOH, если вы избавитесь using namespace std;и используете std::max(f(),g());, компилятор поймает проблему (с неясным сообщением, но, по крайней мере, указывая на сайт вызова).
Руслан
@ Руслан О да. Если дан шанс, это лучший план. Но иногда каждый работает с унаследованным кодом ... (Нет ... не горько. Не горько вообще!)
Cort Ammon
4

Возможно пропустить специализацию шаблона.

// 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);
}
user253751
источник
4

Двоичная несовместимость, доступ к члену или, что еще хуже, вызов функции неправильного класса:

#pragma once

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

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

#endif

Функция использует это, и это нормально:

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

Ввод в другой версии класса:

#pragma once

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

class class_w
{
public: int a;
};

#endif

Используя функции в основном, второе определение изменяет определение класса. Это приводит к двоичной несовместимости и просто дает сбой во время выполнения. И исправьте проблему, удалив первый файл include в 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;
}

Ни один из вариантов не генерирует ошибку времени компиляции или ссылки.

И наоборот, добавление include исправляет сбой:

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

Эти ситуации даже намного сложнее при исправлении ошибки в старой версии программы или при использовании внешнего объекта библиотеки / dll / shared. Вот почему иногда необходимо соблюдать правила двоичной обратной совместимости.

armagedescu
источник
Второй заголовок не будет включен из-за ifndef. В противном случае он не скомпилируется (переопределение класса не допускается).
Игорь Р.
@IgorR. Быть внимательным. Второй заголовок (include1.h) является единственным, включенным в первый исходный код. Это приводит к бинарной несовместимости. Именно в этом заключается цель кода, чтобы проиллюстрировать, как включение может привести к сбою во время выполнения.
armagedescu
1
@IgorR. это очень упрощенный код, который иллюстрирует такую ​​ситуацию. Но в реальной жизни ситуация может быть гораздо более сложной. Попробуйте исправить некоторые программы без переустановки всего пакета. Это типичная ситуация, когда должны строго соблюдаться правила обратной двоичной совместимости. В противном случае исправление является невозможной задачей.
armagedescu
Я не уверен, что такое «первый исходный код», но если вы имеете в виду, что 2 единицы перевода имеют 2 разных определения класса, это нарушение ODR, то есть неопределенное поведение.
Игорь Р.
1
Это неопределенное поведение , как описано в стандарте C ++. FWIW, конечно, можно вызвать UB таким образом ...
Игорь Р.
3

Я хочу отметить, что проблема также существует в C.

Вы можете сказать компилятору, что функция использует некоторые соглашения о вызовах. Если вы этого не сделаете, компилятор должен будет догадаться, что он использует стандартный по умолчанию, в отличие от C ++, где компилятор может отказаться от его компиляции.

Например,

main.c

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

foo.c

#include <stdio.h>

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

На Linux на x86-64 мой вывод

0

Если вы опустите здесь прототип, компилятор предполагает, что у вас есть

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

И соглашение для неопределенных списков аргументов требует, floatчтобы они были преобразованы в doubleпередаваемые. Поэтому, хотя я и дал 1.0f, компилятор преобразует его в, 1.0dчтобы передать foo. И в соответствии с Дополнением к процессору архитектуры AMD64 двоичного интерфейса приложений System V значения doubleпередаются в 64 младших значащих битах xmm0. Но fooожидает float, и он читает его из 32 младших разрядов xmm0и получает 0.

izmw1cfg
источник