เป็นไปได้หรือไม่ที่ #include หายไปเพื่อหยุดโปรแกรมเมื่อรันไทม์?


31

มีกรณีใดบ้างที่ขาดหายไป#includeจะทำให้ซอฟต์แวร์เสียหายขณะรันไทม์ในขณะที่บิลด์ยังดำเนินต่อไป

ในคำอื่น ๆ มันเป็นไปได้ที่

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

และ

complexLogic();
cleverAlgorithms();

ทั้งสองจะสร้างได้สำเร็จ แต่ประพฤติแตกต่างกันอย่างไร


1
อาจรวมถึงของคุณคุณสามารถนำโครงสร้างนิยามใหม่ของรหัสที่แตกต่างจากที่ใช้โดยการใช้งานฟังก์ชั่น สิ่งนี้สามารถนำไปสู่การเข้ากันไม่ได้ของไบนารี สถานการณ์ดังกล่าวไม่สามารถจัดการได้โดยคอมไพเลอร์และโดยลิงเกอร์
armagedescu

11
มันเป็นอย่างแน่นอน มันค่อนข้างง่ายที่จะมีมาโครที่กำหนดไว้ในส่วนหัวที่เปลี่ยนความหมายของรหัสที่มาหลังจากส่วนหัวนั้นคือ#included
ปีเตอร์

4
ฉันแน่ใจว่าCode Golfได้ทำสิ่งที่ท้าทายอย่างน้อยหนึ่งอย่างจากสิ่งนี้
มาร์ค

6
ฉันต้องการจะชี้ให้เห็นตัวอย่างที่แท้จริงของโลก: ไลบรารี VLDสำหรับการตรวจจับการรั่วไหลของหน่วยความจำ เมื่อโปรแกรมยกเลิกการทำงานด้วย VLD มันจะพิมพ์หน่วยความจำที่ตรวจพบทั้งหมดออกมาในช่องสัญญาณออกบางช่อง คุณรวมเข้ากับโปรแกรมโดยเชื่อมโยงไปยังไลบรารี VLD และวางบรรทัดเดียว#include <vld.h>ในตำแหน่งเชิงกลยุทธ์ในรหัสของคุณ การลบหรือเพิ่มส่วนหัว VLD นั้นไม่ได้ "หยุด" โปรแกรม แต่มันมีผลต่อพฤติกรรมของรันไทม์อย่างมีนัยสำคัญ ฉันได้เห็น VLD ทำให้โปรแกรมช้าลงจนถึงขั้นที่ใช้ไม่ได้
Haliburton

คำตอบ:


40

ใช่มันเป็นไปได้อย่างสมบูรณ์แบบ ฉันแน่ใจว่ามีหลายวิธี แต่สมมติว่าไฟล์ include มีคำจำกัดความตัวแปรทั่วโลกซึ่งเรียกว่านวกรรมิก ในกรณีแรกผู้สร้างจะดำเนินการและในครั้งที่สองก็จะไม่

การวางนิยามแบบโกลบอลในไฟล์ส่วนหัวนั้นมีสไตล์ไม่ดี แต่ก็เป็นไปได้


1
<iostream>ในไลบรารีมาตรฐานทำสิ่งนี้อย่างแม่นยำ หากหน่วยใด ๆ รวมถึงการแปล<iostream>แล้วstd::ios_base::Initวัตถุแบบคงที่จะถูกสร้างขึ้นในช่วงเริ่มต้นโปรแกรมการเริ่มต้นตัวอักษรลำธารstd::coutฯลฯ มิฉะนั้นมันจะไม่
ecatmur

33

ใช่มันเป็นไปได้

ทุกสิ่งที่เกี่ยวข้องกับการ#includeเกิดขึ้นในเวลารวบรวม แต่สิ่งที่เวลารวบรวมสามารถเปลี่ยนพฤติกรรมที่รันไทม์แน่นอน:

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

เพียงเพื่อชี้ให้เห็นกรณีเล็ก ๆ น้อย ๆ คำสั่ง 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;
}

และจากนั้น

// 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 () ไม่ใช่ idempotent สิ่งนี้อาจทำให้เกิดปัญหาได้ ตัวอย่างเช่นหากหนึ่งในนั้นเกิดขึ้นเป็นเคาน์เตอร์ซึ่งจะส่งกลับจำนวนที่แตกต่างกันทุกครั้ง ....


+1 สำหรับการโทรออกฟังก์ชั่นสูงสุดของ Window ตัวอย่างของโลกแห่งความเป็นจริงรวมถึงการนำความชั่วร้ายและความหายนะมาสู่การพกพาได้ทุกที่
Scott M

3
OTOH ถ้าคุณกำจัดusing namespace std;และใช้std::max(f(),g());คอมไพเลอร์จะตรวจจับปัญหา (ด้วยข้อความที่คลุมเครือ แต่อย่างน้อยก็ชี้ไปที่ไซต์โทร)
Ruslan

@ รุสลันโอ้ใช่ หากได้รับโอกาสนั่นเป็นแผนการที่ดีที่สุด แต่บางครั้งก็ทำงานกับรหัสเดิม ... (ไม่ ... ไม่ขมไม่ขมเลย!)
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);
}

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 มิฉะนั้นจะไม่คอมไพล์ (ไม่อนุญาตการกำหนดคลาสใหม่)
Igor R.

@IgorR เป็นคนที่เอาใจใส่ ส่วนหัวที่สอง (include1.h) จะรวมอยู่ในซอร์สโค้ดแรกเท่านั้น สิ่งนี้นำไปสู่ความไม่ลงรอยกันของไบนารี นั่นคือจุดประสงค์ของรหัสเพื่ออธิบายวิธีการรวมสามารถนำไปสู่ความผิดพลาดที่รันไทม์
armagedescu

1
@IgorR นี่เป็นรหัสอย่างง่ายซึ่งแสดงให้เห็นถึงสถานการณ์เช่นนี้ แต่ในชีวิตจริงอาจมีความซับซ้อนมากกว่า ลองทำการปะบางโปรแกรมโดยไม่ต้องติดตั้งแพคเกจทั้งหมดอีกครั้ง เป็นสถานการณ์ทั่วไปที่ต้องปฏิบัติตามกฎความเข้ากันได้แบบไบนารีย้อนหลังอย่างเคร่งครัด มิฉะนั้นการแก้ไขคืองานที่เป็นไปไม่ได้
armagedescu

ฉันไม่แน่ใจว่า "ซอร์สโค้ดแรก" คืออะไร แต่ถ้าคุณหมายความว่าหน่วยแปล 2 หน่วยมีคำจำกัดความที่แตกต่างกัน 2 คลาสมันเป็นการละเมิด ODR นั่นคือพฤติกรรมที่ไม่ได้กำหนดไว้
Igor R.

1
นั่นคือพฤติกรรมที่ไม่ได้กำหนดตามที่อธิบายโดยมาตรฐาน C ++ แน่นอนว่า FWIW เป็นไปได้ที่จะทำให้เกิด UB ด้วยวิธีนี้ ...
Igor R.

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และเป็นไปตามระบบวี Application Binary Interface AMD64 สถาปัตยกรรมเสริมตัวประมวลผลที่doubleได้รับผ่านใน 64 xmm0บิตอย่างมีนัยสำคัญน้อย แต่fooคาดว่าจะลอยและมันอ่านจาก 32 บิตที่สำคัญน้อยที่สุดxmm0และได้รับ 0

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.