如何把「信任」局部化到真實程式裡
繁中(台灣)

如何把「信任」局部化到真實程式裡

前兩節解釋了 [[scpp::unsafe]] 到底替哪些事情開門,以及裸指標、extern "C" 呼 叫是如何跨過這道邊界的。

這一節要回答下一個更實際的問題:在真實程式裡,unsafe 程式碼應該放在哪裡?

總規則其實很簡單:

下面每個可執行範例都可以存成 trust.scpp,然後這樣建置並執行:

scpp trust.scpp -o trust
./trust

對於那些本來就應該被編譯器拒絕的範例,如果你希望得到與書裡逐字一致的診斷輸 出,請把檔案存成診斷區塊裡顯示的那個描述性檔名。

優先在普通安全函式裡放一個很小的 unsafe block

如果一個函式本身就能完全控制並證明那次 unsafe 操作是合理的,那麼就讓這個函式 保持普通,只把真正關鍵的那一步放進 unsafe。

import std;

int first_of_pair(int left, int right) {
    int values[2]{};
    values[0] = left;
    values[1] = right;
    int* pointer = &values[0];
    [[scpp::unsafe]] {
        return *pointer;
    }
}

int main() {
    std::println("{}", first_of_pair(11, 22));
    return 0;
}

輸出:

11

這裡,呼叫端根本不需要知道裸指標的存在。這個函式自己建立了區域陣列、形成了指 標,也只在一個很小的位置解參考了它,所以它可以直接為那一步負責。

當呼叫端必須擔保時,使用函式層級的 [[scpp::unsafe]]

有時候,函式主體自己無法獨立讓操作變得可靠。如果函式接收的是外部給來的裸指 標,那麼它的正確性就取決於一個只有呼叫端才能保證的前提條件。

import std;

[[scpp::unsafe]] int read_first(int* pointer) {
    return *pointer;
}

int main() {
    int value{9};
    [[scpp::unsafe]] {
        std::println("{}", read_first(&value));
    }
    return 0;
}

輸出:

9

這裡的 unsafe,不再只是內部實作細節,而是這個函式契約的一部分。

這個契約會傳播到呼叫點

因為真正需要擔保輸入的人是呼叫端,所以從安全程式碼裡直接呼叫這種函式,會被編 譯器拒絕。

[[scpp::unsafe]] int read_first(int* pointer) {
    return *pointer;
}

int main() {
    int value{9};
    return read_first(&value);
}

編譯器輸出:

call_unsafe_wrapper_outside_unsafe_fail.scpp:7:12: error: cannot call 'read_first' outside '[[scpp::unsafe]] { }': its own declaration is marked '[[scpp::unsafe]]', so its soundness depends on a precondition only the caller can guarantee (ch01 §1.2/§1.3)
 7 |     return read_first(&value);
   |            ^

所以,函式層級的 [[scpp::unsafe]] 應該被看成一個很明確的 API 設計決定。它會 讓呼叫端也分擔安全論證的責任。

即使在較大的封裝裡,也要把 unsafe 邊界壓窄

真實程式裡,往往需要連續呼叫幾個外部函式,但規則仍然一樣:讓每個 unsafe 區域 盡量貼近真正需要它的那一次呼叫,讓其餘邏輯繼續保持普通安全程式碼。

import std;

extern "C" {
    int socket(int domain, int type, int protocol);
    int getsockopt(int fd, int level, int optname, void* optval, int* optlen);
    int close(int fd);
}

int query_socket_type() {
    int fd = 0;
    [[scpp::unsafe]] {
        fd = socket(2, 2, 0);
    }

    int value = 0;
    int len = 4;
    [[scpp::unsafe]] {
        getsockopt(fd, 1, 3, &value, &len);
        close(fd);
    }
    return value;
}

int main() {
    std::println("{}", query_socket_type());
    return 0;
}

輸出:

2

query_socket_type 的大部分程式碼仍然是普通程式碼:區域變數、傳回值和控制流程 都沒有變。真正被圍起來的,只有那些外部呼叫本身。

即使整個 unsafe 區域很大,檢查器也仍然開著

把整個程式碼區塊標成 unsafe,並不代表關閉所有權檢查。即使有時你確實需要一 個較寬的 unsafe 區域,scpp 仍然會繼續檢查 move 和 borrow。

import std;

int f() {
    [[scpp::unsafe]] {
        std::unique_ptr<int> first = std::make_unique<int>(1);
        std::unique_ptr<int> second = std::move(first);
        std::unique_ptr<int> third = std::move(first);
        return *third;
    }
}

int main() {
    return f();
}

編譯器輸出:

unsafe_whole_body_still_checks_moves_fail.scpp:7:38: error: use of moved-out variable 'first'
 7 |         std::unique_ptr<int> third = std::move(first);
   |                                      ^

這才是「把信任局部化」的真正含義:unsafe 的部分應該只包含那些編譯器確實無法自 行證明的地方,其餘部分仍然應該盡量留在普通受檢查的世界裡。

下一章會離開這些底層邊界,轉向專案結構:套件、模組與資訊清單檔。


← 上一章:呼叫 extern "C" 與使用裸指標 · 目錄