用生命周期验证引用
中文

用生命周期验证引用

上一节说明了:泛型 API 可以描述一个类型必须支持哪些操作。但只要 API 接受或返回 引用,就还会多出一个问题:它要怎样描述这些借用是从哪里来的

在今天的 scpp 里,这件事由 [[scpp::lifetime(...)]] 标注负责。它们可以让泛型函数和 concept probe 描述多个引用参数之间的关系,也可以让某些 callback 风格 API 表达“这个 引用是新鲜创建出来的,而且只在这次调用期间有效”。

下面每个可运行示例都请保存为 lifetimes.scpp,然后这样构建并运行:

scpp lifetimes.scpp -o lifetimes
./lifetimes

泛型函数可以指明返回引用来自哪一个输入引用

当一个泛型函数返回引用时,生命周期标注可以说明:这个结果到底绑在哪个输入引用上。

import std;

template<typename T>
const T& keep_left(
    const T& left [[scpp::lifetime(a)]],
    const T& right [[scpp::lifetime(b)]]
) [[scpp::lifetime(a)]] {
    return left;
}

int main() {
    int first = 31;
    int second = 8;
    const int& kept = keep_left(first, second);
    int value = kept;
    std::println("{}", value);
    return 0;
}

输出:

31

这里真正重要的不是具体的 int,而是这个关系:返回出来的引用被明确绑定到 a 组参 数,而不是 b 组参数。

concept probe 也可以检查多个引用之间的生命周期关系

concept 不只会检查操作名和返回型别。probe 也可以描述:多个引用参数之间必须是什么关 系。

import std;

class Token {
public:
    virtual ~Token() { return; }

public:
    int value{};
};

class Adder {
public:
    virtual ~Adder() { return; }

public:
    int add(Token& a [[scpp::lifetime(shared)]], Token& b [[scpp::lifetime(shared)]]) const {
        return a.value + b.value;
    }
};

template<typename T>
concept AddsPair = requires(T c, Token& x [[scpp::lifetime(p)]], Token& y [[scpp::lifetime(p)]]) {
    { c.add(x, y) } -> std::same_as<int>;
};

int use_it(const AddsPair auto& adder, Token& left, Token& right) {
    return adder.add(left, right);
}

int main() {
    Adder adder{};
    Token first{};
    first.value = 3;
    Token second{};
    second.value = 4;
    int sum = use_it(adder, first, second);
    std::println("{}", sum);
    return 0;
}

输出:

7

这个 probe 把 xy 都放进 p 这一组里。Adder::add() 里用的是另外一个拼写 shared,但这没有关系:重点是,这两个声明里都表达了“这两个参数属于同一个共享组”。

[[scpp::lifetime(any)]] 可以让 callback 接受一次新鲜创建出来的借用

有些泛型 API 会在自己的函数体内部创建一个值,然后把它的引用传给 callback。对这种模 式来说,callback 不可能事先写出那个具体生命周期的名字,因为这个生命周期是由 callee 当场创造出来的。

import std;

class Token {
public:
    virtual ~Token() { return; }

public:
    int value{};
};

template<typename T>
concept AcceptsToken = requires(T callback, Token& tok [[scpp::lifetime(any)]]) {
    { callback(tok) } -> std::same_as<void>;
};

void with_fresh_token(AcceptsToken auto&& callback) {
    Token tok{};
    tok.value = 42;
    callback(tok);
    return;
}

int main() {
    int result = 0;
    with_fresh_token([&result](Token& tok [[scpp::lifetime(any)]]) {
        result = tok.value;
        return;
    });
    std::println("{}", result);
    return 0;
}

输出:

42

保留组名 any 的意思是“这次调用现场新造出来的一个生命周期”。这让包装函数可以把自 己局部 Token 的引用传进去,同时 callback 仍然不能把这个借用当成某个更长寿命的命 名分组来使用。

今天 scpp 里关于泛型 API 生命周期的工作规则

到目前为止,实用规则可以先记成这样:

这样一来,第 10 章开头需要的工具就齐了:泛型定义、concept 描述的操作要求,以及用于 引用的生命周期标注。下一章会在这套基础上继续讲闭包:看看局部 callback 对象怎样把捕 获、concept 与生命周期放进普通用户代码里。


← 上一章:用 concept 描述共享要求 · 目录 · 下一章:闭包与捕获 →