Apakah mungkin #include yang hilang untuk menghentikan program saat runtime?

31

Apakah ada kasus, di mana yang hilang #includeakan merusak perangkat lunak saat runtime, sementara build masih berjalan?

Dengan kata lain, mungkinkah itu

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

dan

complexLogic();
cleverAlgorithms();

akan sama-sama membangun dengan sukses, tetapi berperilaku berbeda?

Antti_M
sumber
1
Mungkin dengan menyertakan Anda, Anda bisa membawa kode Anda mendefinisikan ulang struktur yang berbeda dari yang digunakan oleh implementasi fungsi. Ini dapat menyebabkan ketidakcocokan biner. Situasi seperti itu tidak dapat ditangani oleh kompiler dan oleh penghubung.
armagedescu
11
Tentu saja. Cukup mudah memiliki makro yang didefinisikan di header yang sepenuhnya mengubah arti kode yang muncul setelah header itu #included.
Peter
4
Saya yakin Code Golf telah melakukan setidaknya satu tantangan berdasarkan ini.
Mark
6
Saya ingin menunjukkan contoh nyata di dunia nyata: Perpustakaan VLD untuk deteksi kebocoran memori. Ketika sebuah program berakhir dengan VLD aktif, itu akan mencetak semua kebocoran memori yang terdeteksi pada beberapa saluran keluaran. Anda mengintegrasikannya ke dalam program dengan menautkan ke perpustakaan VLD dan menempatkan satu baris #include <vld.h>dalam posisi strategis dalam kode Anda. Menghapus atau menambahkan header VLD itu tidak "merusak" program, tetapi itu mempengaruhi perilaku runtime secara signifikan. Saya telah melihat VLD memperlambat program hingga tidak dapat digunakan lagi.
Haliburton

Jawaban:

40

Ya, sangat mungkin. Saya yakin ada banyak cara, tetapi anggaplah file include berisi definisi variabel global yang disebut konstruktor. Dalam kasus pertama, konstruktor akan mengeksekusi, dan yang kedua tidak.

Menempatkan definisi variabel global dalam file header adalah gaya yang buruk, tetapi itu mungkin.

john
sumber
1
<iostream>di perpustakaan standar tidak tepat ini; jika ada unit terjemahan termasuk <iostream>maka std::ios_base::Initobjek statis akan dibangun pada awal program, menginisialisasi aliran karakter std::coutdll, jika tidak maka tidak akan.
ecatmur
33

Ya itu mungkin.

Segala sesuatu yang berkaitan #includeterjadi pada waktu kompilasi. Tetapi kompilasi waktu hal-hal dapat mengubah perilaku saat runtime, tentu saja:

some/code.h:

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

kemudian

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

Dengan #include, resolusi kelebihan menemukan yang lebih tepat foo(int)dan karenanya mencetak 1daripada 2. Juga, sejak FOOdidefinisikan, ia juga mencetak FOO.

Itu hanya dua contoh (tidak berhubungan) yang langsung muncul di benak saya, dan saya yakin ada banyak lagi.

pasbi
sumber
14

Hanya untuk menunjukkan kasus sepele, arahan precompiler:

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

Lalu

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

Ini patologis, mungkin, tapi saya punya kasus terkait terjadi:

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

using namespace std;

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

Terlihat tidak berbahaya. Mencoba menelepon std::max. Namun, windows.h mendefinisikan max menjadi

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

Jika ini std::max, ini akan menjadi panggilan fungsi normal yang mengevaluasi f () sekali dan g () sekali. Tetapi dengan windows.h di sana, sekarang mengevaluasi f () atau g () dua kali: sekali selama perbandingan dan sekali untuk mendapatkan nilai kembali. Jika f () atau g () tidak idempoten, ini dapat menyebabkan masalah. Misalnya, jika salah satu dari mereka kebetulan adalah penghitung yang mengembalikan nomor yang berbeda setiap kali ....

Cort Ammon
sumber
+1 untuk memanggil fungsi max Window, contoh dunia nyata dari menyertakan implementasi evil dan bane to portability di mana saja.
Scott M
3
OTOH, jika Anda menyingkirkan using namespace std;dan menggunakan std::max(f(),g());, kompiler akan menangkap masalah (dengan pesan yang tidak jelas, tetapi setidaknya menunjuk ke situs panggilan).
Ruslan
@Ruslan Oh, ya. Jika diberi kesempatan, itulah rencana terbaik. Tetapi kadang-kadang seseorang bekerja dengan kode warisan ... (tidak ... tidak pahit. Tidak pahit sama sekali!)
Cort Ammon
4

Mungkin saja ada spesialisasi template yang hilang.

// 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);
}
pengguna253751
sumber
4

Ketidakcocokan biner, mengakses anggota atau bahkan lebih buruk, memanggil fungsi dari kelas yang salah:

#pragma once

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

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

#endif

Sebuah fungsi menggunakannya, dan tidak apa-apa:

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

Membawa versi lain dari kelas:

#pragma once

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

class class_w
{
public: int a;
};

#endif

Menggunakan fungsi di main, definisi kedua mengubah definisi kelas. Ini menyebabkan ketidakcocokan biner dan hanya crash saat runtime. Dan perbaiki masalah dengan menghapus menyertakan pertama di 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;
}

Tidak ada varian yang menghasilkan kesalahan waktu kompilasi atau tautan.

Situasi sebaliknya, menambahkan sertakan perbaikan kerusakan:

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

Situasi ini bahkan jauh lebih sulit ketika memperbaiki bug di versi program lama, atau menggunakan perpustakaan eksternal / dll / objek bersama. Itu sebabnya kadang-kadang harus diikuti aturan kompatibilitas mundur biner.

armagedescu
sumber
Header kedua tidak akan dimasukkan karena ifndef. Kalau tidak, itu tidak akan dikompilasi (redefining kelas tidak diperbolehkan).
Igor R.
@IgorR. Menjadi perhatian. Header kedua (include1.h) adalah satu-satunya yang dimasukkan dalam kode sumber pertama. Ini menyebabkan ketidakcocokan biner. Itulah tujuan dari kode, untuk menggambarkan bagaimana sebuah menyertakan dapat menyebabkan crash saat runtime.
armagedescu
1
@IgorR. ini adalah kode yang sangat sederhana, yang menggambarkan situasi seperti itu. Tetapi dalam situasi kehidupan nyata bisa jauh lebih rumit subtil. Coba tambal beberapa program tanpa menginstal ulang seluruh paket. Ini adalah situasi umum di mana harus diikuti secara ketat aturan kompatibilitas biner mundur. Kalau tidak, menambal adalah tugas yang mustahil.
armagedescu
Saya tidak yakin apa "kode sumber pertama" itu, tetapi jika Anda bermaksud bahwa 2 unit terjemahan memiliki 2 definisi kelas yang berbeda, itu adalah pelanggaran ODR, yaitu perilaku yang tidak terdefinisi.
Igor R.
1
Itu adalah perilaku yang tidak terdefinisi , seperti yang dijelaskan oleh Standar C ++. FWIW, tentu saja, dimungkinkan untuk menyebabkan UB dengan cara ini ...
Igor R.
3

Saya ingin menunjukkan bahwa masalahnya juga ada di C.

Anda dapat memberi tahu kompiler bahwa suatu fungsi menggunakan beberapa konvensi pemanggilan. Jika tidak, kompiler harus menebak bahwa ia menggunakan yang default, tidak seperti di C ++ di mana kompiler dapat menolak untuk mengkompilasinya.

Sebagai contoh,

main.c

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

foo.c

#include <stdio.h>

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

Di Linux pada x86-64, output saya adalah

0

Jika Anda menghilangkan prototipe di sini, kompilator mengasumsikan Anda memilikinya

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

Dan konvensi untuk daftar argumen yang tidak ditentukan mensyaratkan bahwa floatharus dikonversi doubleagar dapat disahkan. Jadi walaupun saya berikan 1.0f, kompiler mengkonversinya 1.0duntuk meneruskannya foo. Dan menurut System V Application Binary Interface, Tambahan Prosesor Arsitektur AMD64, doubleakan dilewatkan dalam 64 bit paling tidak signifikan xmm0. Tetapi foomengharapkan float, dan membacanya dari 32 bit paling tidak signifikan xmm0, dan mendapat 0.

izmw1cfg
sumber