לדלג לתוכן

פתרון גבולות ספרייה ותאימות

ההרחבה של pImpl משנה את קובץ המימוש. מוסיפים unsigned updates = 0; למבנה הפנימי, ומגדילים אותו רק אחרי שבדיקת הגבול הצליחה. בחירה סבירה היא לספור גם הוספה של אפס כקריאה מוצלחת, כל עוד הבחירה מתועדת. הבדיקה צריכה להראות שכשל אינו מגדיל את המונה.

המימוש הבא מצטרף ל־header של ממשק ה־C מההרצאה. הוא עצמאי ממחלקת Counter, כדי להראות שהלקוח תלוי בחוזה בלבד.

#include "counter_api.h"
#include <new>

struct counter_state {
    int32_t total = 0;
};

extern "C" uint32_t counter_api_version(void) {
    return 1;
}

extern "C" int counter_create(counter_handle* out) {
    if (!out) return COUNTER_INVALID;
    *out = nullptr;
    try {
        *out = new counter_state;
        return COUNTER_OK;
    } catch (const std::bad_alloc&) {
        return COUNTER_MEMORY;
    } catch (...) {
        return COUNTER_INTERNAL;
    }
}

extern "C" int counter_add(counter_handle value, int32_t amount) {
    if (!value) return COUNTER_INVALID;
    if (amount < 0 || amount > 1000 - value->total)
        return COUNTER_LIMIT;
    value->total += amount;
    return COUNTER_OK;
}

extern "C" int counter_read(counter_handle value, int32_t* out) {
    if (!value || !out) return COUNTER_INVALID;
    *out = value->total;
    return COUNTER_OK;
}

extern "C" void counter_destroy(counter_handle value) {
    delete value;
}

הבדיקה של amount > 1000 - total נמנעת מחיבור שעלול לגלוש לפני בדיקת המגבלה. האינווריאנט הוא שהסכום נמצא תמיד בין אפס לאלף. מכיוון שהסכום חוקי לפני הפעולה, החיסור שבבדיקה חוקי אף הוא.

הלקוח הבא נשמר ב־client.cpp. בונים את הקובץ יחד עם המימוש, בלי DLL בשלב זה.

#include "counter_api.h"
#include <cassert>

int main() {
    counter_handle handle = nullptr;
    assert(counter_create(&handle) == COUNTER_OK);
    assert(counter_add(handle, 600) == COUNTER_OK);
    assert(counter_add(handle, 500) == COUNTER_LIMIT);
    int32_t result = -1;
    assert(counter_read(handle, &result) == COUNTER_OK);
    assert(result == 600);
    result = 42;
    assert(counter_read(nullptr, &result) == COUNTER_INVALID);
    assert(result == 42);
    counter_destroy(handle);
    handle = nullptr;
    counter_destroy(handle);
}

הבדיקות משתמשות ב־assert לצורך הלימוד, ולכן יש לבנות ללא NDEBUG. במערכת בדיקות אמיתית אין להסתמך על ביטוי בעל תופעת לוואי בתוך assert שנעלם ב־release.

הערך של handle הוא כתובת, ולא הוכחה לחיים. אחרי ההשמדה אותה כתובת יכולה להישאר במשתנה ואף לשמש בהמשך הקצאה אחרת. הממשק אינו מתחייב לזהות שימוש חוזר אחרי השמדה. אם נדרשת הגנה מפני לקוחות פנימיים שוגים, אפשר לתכנן טבלת מזהים עם generation, אבל גם אז צריך להגדיר סנכרון ועלות.

השינוי ב־Impl אינו משנה את גודל המחלקה הציבורית, אך עדיין צריך לבדוק את ABI של הפונקציות ואת סביבת הכלים. שינוי המגבלה יכול להיות תואם ברמת מקור ובינארי, ובכל זאת לשבור התנהגות שהלקוח הסתמך עליה. שינוי רוחב פרמטר בפונקציה קיימת מחייב ממשק חדש; התאמה מקרית של רגיסטרים אינה חוזה.

החלק החשוב בבדיקה הוא שמירת הלקוח הישן. בנייה מחדש של כולם יכולה להסתיר בדיוק את השבר שרצינו לחשוף.