Чи можливо для відсутнього #include зламати програму під час виконання?


31

Чи є випадок, коли пропущений A #includeпорушить програмне забезпечення під час виконання, тоді як збірка все ще проходить?

Іншими словами, чи можливо це?

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

і

complexLogic();
cleverAlgorithms();

обидва б будували успішно, але поводилися б інакше?


1
Ймовірно, з вашими включеннями ви могли б ввести у свій код переосмислені структури, які відрізняються від тих, що використовуються при реалізації функцій. Це може призвести до бінарної несумісності. Такі ситуації не можуть вирішувати компілятор і посилання.
Армагедеску

11
Це, безумовно, є. Досить легко мати макроси, визначені в заголовку, які повністю змінюють значення коду, що надходить після цього заголовка #included.
Петро

4
Я впевнений, що Code Golf зробив хоча б один виклик на основі цього.
Марк

6
Я хотів би зазначити конкретний приклад у реальному світі: бібліотека VLD для виявлення витоку пам'яті. Коли програма завершується, коли VLD активна, вона видасть усі виявлені витоки пам'яті на деякому вихідному каналі. Ви інтегруєте його в програму, підключившись до бібліотеки VLD і розмістивши в #include <vld.h>коді одну лінію на стратегічному положенні. Видалення або додавання того, що заголовок VLD не "порушує" програму, але суттєво впливає на поведінку виконання. Я бачив, як VLD сповільнює програму до того, що вона стала непридатною.
Галібуртон

Відповіді:


40

Так, це цілком можливо. Я впевнений, що існує маса способів, але припустимо, що файл include містив глобальне визначення змінної, яке називається конструктором. У першому випадку конструктор буде виконувати, а в другому - ні.

Поставити визначення загальної змінної у файл заголовка - це поганий стиль, але це можливо.


1
<iostream>у стандартній бібліотеці робить саме це; якщо будь-який блок перекладу включає, <iostream>то std::ios_base::Initстатичний об'єкт буде побудований при запуску програми, ініціалізуючи потоки символів std::coutтощо, інакше це не буде.
екатмур

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.

Це лише два (непов'язані) приклади, які мені відразу прийшли до тями, і я впевнений, що їх ще багато.


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 визначає макс

#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());, компілятор вирішить проблему (із незрозумілим повідомленням, але принаймні вказуючи на сайт виклику).
Руслан

@ Руслан О, так. Якщо дано шанс, це найкращий план. Але іноді хтось працює зі спадковим кодом ... (ні ... не зовсім гіркий. Не гіркий!)
Корт Аммон

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

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

Використовуючи основні функції, друге визначення змінює визначення класу. Це призводить до бінарної несумісності і просто виходить з ладу під час виконання. І усуньте проблему, видаливши перше включення до 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;
}

Жоден із варіантів не створює помилку часу компіляції чи посилання.

Навпаки ситуація, додаючи включає, виправляє збій:

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

Ці ситуації ще набагато складніші під час виправлення помилки в старій версії програми або використання зовнішньої бібліотеки / dll / спільного об'єкта. Ось чому іноді слід дотримуватися правил бінарної сумісності назад.


Другий заголовок не буде включений через ifndef. Інакше вона не компілюється (переосмислення класу не дозволено).
Ігор Р.

@IgorR. Будьте уважні. Другий заголовок (include1.h) є єдиним, що входить до першого вихідного коду. Це призводить до бінарної несумісності. Саме це і є метою коду, щоб проілюструвати, як включення може призвести до збоїв під час виконання.
armagedescu

1
@IgorR. це дуже спрощений код, який ілюструє таку ситуацію. Але в реальному житті ситуація може бути набагато складнішою субтилею. Спробуйте виправити якусь програму, не перевстановлюючи весь пакет. Це типова ситуація, коли слід суворо дотримуватись відсталих правил бінарної сумісності. Інакше виправлення - це неможливе завдання.
armagedescu

Я не впевнений, що таке "перший вихідний код", але якщо ви маєте на увазі, що 2 перекладацькі одиниці мають 2 різні визначення класу, це порушення ODR, тобто невизначена поведінка.
Ігор Р.

1
Це невизначена поведінка , як описано Стандартом C ++. FWIW, звичайно, можна викликати UB таким чином ...
Ігор Р.

3

Хочу зазначити, що проблема існує і в С.

Ви можете сказати компілятору, що функція використовує певну умову виклику. Якщо цього не зробити, компілятору доведеться здогадуватися, що він використовує типовий, на відміну від 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. І відповідно до додатку System V Binary Interface AMD64 Архітектурний процесор Доповнення, отримані doubleпередачі в 64 найменш значущих бітах xmm0. Але fooочікує, що поплавок є, і він зчитує його з 32 найменш значущих бітів xmm0і отримує 0.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.