好的做法:什么时候非变异函数要求指针而不是副本?

问题描述 投票:0回答:1

考虑一个非常轻的数据类型,以及一个将此类型作为参数但不改变它的函数 - 我试图理解何时我想要将指针传递给它而不是简单地复制。就一般C良好做法而言。

在我看来,复制这样一个轻量值对于性能来说并不重要,并且指针可能会导致比普通值更多的混淆。

例如,请考虑以下代码(取自Bob Nystrom“Crafting Interpreters”一书):

typedef struct {
    TokenType type;
    const char* start;
    int length;
    int line;
} Token;

在下面的代码中,identifiersEqual采用Token*类型的参数而不是纯Token。这可能有意义 - 我们不必复制Token

另一方面,addLocal采取简单的Token

在一般的C良好实践方面 - 我试图理解是否有一个特殊的原因,为什么identifiersEqual采取指针,但addLocal采取副本。这两个函数都没有改变这个值,而且再一次 - Token并不重要。

这里有一种我失踪的模式,还是这只是偶然的?我应该在什么情况下以这种方式或另一种方式来决定?

static bool identifiersEqual(Token* a, Token* b) {
    if (a->length != b->length) return false;
    return memcmp(a->start, b->start, a->length) == 0;
}

static void addLocal(Token name) {
    if (current->localCount == UINT8_COUNT) {
        error("Too many local variables in function.");
        return;
    }

    Local* local = &current->locals[current->localCount++];
    local->name = name;
    local->depth = -1;
}

static void declareVariable() {
    if (current->scopeDepth == 0) return;

    Token* name = &parser.previous;

    for (int i = current->localCount - 1; i >= 0; i--) {
        Local* local = &current->locals[i];
        if (local->depth != -1 && local->depth < current->scopeDepth) break;
        if (identifiersEqual(name, &local->name)) {
            error("Variable with this name already declared in this scope.");
        }
    }

    addLocal(*name);
}

c pointers copy parameter-passing conventions
1个回答
1
投票

这个问题正在征求意见,所以我应该避免说我可能不会在同一个程序中使用这两个接口,因为我试图避免使用数据类型有时通过值传递的API,其他时候通过参考。但那只是我,所以我会将答案的其余部分限制为您可能选择使用按值传递中等重量对象的接口的原因。如果你想听原始编码员选择这种风格的原因,你应该直接问他。

第一点是,如果大多数现代编译器可以访问被调用函数的主体,并且被调用函数本身足够轻巧,可以内联,则可以避免副本。这些条件似乎适用于引用代码中的函数,因此使用call-by-value可能没有任何成本。 [注1]因此,如果API样式为代码阅读器提供了有用的信息,那么它可能被认为是有用的。

现在考虑参数原型X const *X。在这两种情况下,我们都知道传递的参数不会被修改,所以我们当然不需要复制它。

但是仍然可能会对争论的生命周期产生担忧。如果被调用的函数接受一个指针并将该指针保存到一个比调用寿命更长的对象中,那么我们需要担心传递的对象的所有权。实际上,我们需要将对象的所有权传递给被调用的函数,我们还需要确保对象没有自动生命周期。特别是,我们无法使用临时函数调用该函数,我们可能会对使用具有静态生命周期的对象(不能是freed)调用该函数感到怀疑。

另一方面,按值调用显然不会对呼叫者施加任何要求。如果被调用函数想要保存传递​​的对象,则它负责在副本不再有用时进行复制和处理。我们可以传递任何我们喜欢的对象:临时,静态或本地对象,它们将被重用于后续调用。

实际上,令牌对象通常是在解析循环中重用的本地对象,而不是动态分配的对象,这些对象强加了更复杂的内存管理机制。大多数情况下,只会查询传递给函数的令牌对象,但有时它们确实需要保存。函数名称addLocal强烈建议此函数将持久保存传递的对象。

在这种特殊情况下,addLocal确实保存了传递的对象,但它保存了一个副本。它不能这样做,因为它传递了一个副本,并且副本不会超过调用。幸运的是,优化器几乎肯定会内联addLocal,从而避免不必要的中间副本。因此,在这里使用call-by-value已经准确地告知代码阅读器,完全没有必要担心传递给addLocal的对象的生命周期。

identifiersEqual的情况下,被调用函数似乎不太可能需要持久保存任何传递的对象,因此保证可能不那么重要。但是,如上所述,为了保持一致性,我可能已经将identifiersEqual编写为call-by-value,希望编译器成功完全避免复制。 (这是一种信念的飞跃,我在这里寻求一致性的可能性是一种抽象。)


Notes

  1. 对于非常轻量级的对象和某些编译器,可以通过call-by-value生成更好的编译代码。例如,标准的64位ABI允许适合八个字节的结构在寄存器中传递,如果构造对象的唯一目的是将其传递给函数,这是特别方便的。我记得从我写入OS X GUI API的那些日子开始,小的几何对象总是按值传递小的几何对象,并且编程指南中有一个注释说明这是为了提高效率。我不知道这是否仍然是正确的,但我也不认为这个特定的结构足够轻量级以便应用。尽管可能并不常见,但还有其他一些上下文使得某些编译器明确表示从不采用对象的地址确实允许编译器生成更好的代码。 在这个问题的上下文中,足以观察到生成的按值调用的代码(可能)并不比调用引用更差。如果它真的对你很重要,你必须检查你关心的编译器生成的代码。我没有这样做。
© www.soinside.com 2019 - 2024. All rights reserved.